260720
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
unpackage/
|
||||||
|
.hbuilderx/
|
||||||
|
.DS_Store
|
||||||
|
/.claude/
|
||||||
|
/.idea/
|
||||||
33
App.vue
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
onLaunch: function() {
|
||||||
|
console.log('App Launch')
|
||||||
|
},
|
||||||
|
onShow: function() {
|
||||||
|
console.log('App Show')
|
||||||
|
},
|
||||||
|
onHide: function() {
|
||||||
|
console.log('App Hide')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "@/uni_modules/uv-ui-tools/index.scss";
|
||||||
|
|
||||||
|
page {
|
||||||
|
background: #f4faff;
|
||||||
|
color: #1f2329;
|
||||||
|
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
view,
|
||||||
|
scroll-view,
|
||||||
|
swiper,
|
||||||
|
swiper-item,
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
195
components/common-hero/common-hero.vue
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
<template>
|
||||||
|
<view class="hero-section" :style="heroStyle">
|
||||||
|
<!-- <view class="hero-bg hero-bg-left"></view>-->
|
||||||
|
<!-- <view class="hero-bg hero-bg-right"></view>-->
|
||||||
|
|
||||||
|
<view class="navbar" :style="navbarStyle">
|
||||||
|
<view class="navbar-inner">
|
||||||
|
<view class="navbar-left" v-if="showBack">
|
||||||
|
<view class="nav-back-btn" @tap="handleBack">
|
||||||
|
<uv-icon name="arrow-left" color="#101214" size="22"></uv-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="navbar-title">{{ title }}</view>
|
||||||
|
<view class="navbar-right-placeholder"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="navbar-spacer" :style="{ height: navbarHeight + 'px' }"></view>
|
||||||
|
|
||||||
|
<slot></slot>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const TAB_BAR_PATHS = ['/pages/index/index', '/pages/assistant/index', '/pages/mine/index']
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CommonHero',
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
useImageBg: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
backgroundImage: {
|
||||||
|
type: String,
|
||||||
|
default: '/static/indexBg.jpg'
|
||||||
|
},
|
||||||
|
reduceTop: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
statusBarHeight: 0,
|
||||||
|
currentRoute: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
showBack() {
|
||||||
|
return TAB_BAR_PATHS.indexOf(this.currentRoute) === -1
|
||||||
|
},
|
||||||
|
navbarHeight() {
|
||||||
|
return this.statusBarHeight + uni.upx2px(88) - uni.upx2px(this.reduceTop)
|
||||||
|
},
|
||||||
|
navbarStyle() {
|
||||||
|
return {
|
||||||
|
paddingTop: `${this.statusBarHeight}px`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
heroStyle() {
|
||||||
|
if (this.useImageBg) {
|
||||||
|
return {
|
||||||
|
backgroundImage: `url('${this.backgroundImage}')`,
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
backgroundPosition: 'center top',
|
||||||
|
backgroundRepeat: 'no-repeat'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
background: 'linear-gradient(180deg, #f7fbff 0%, #d9f0ff 44%, #f4fbff 100%)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const { statusBarHeight = 0 } = uni.getSystemInfoSync()
|
||||||
|
this.statusBarHeight = statusBarHeight
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
const currentPage = pages[pages.length - 1]
|
||||||
|
const route = currentPage && currentPage.route ? currentPage.route : ''
|
||||||
|
this.currentRoute = route ? `/${route}` : ''
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleBack() {
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
if (pages.length > 1) {
|
||||||
|
uni.navigateBack({
|
||||||
|
delta: 1
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.hero-section {
|
||||||
|
position: relative;
|
||||||
|
//padding: 0 0 30rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
backgroud: url('@/static/indexBg.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-bg {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(8rpx);
|
||||||
|
opacity: 0.9;
|
||||||
|
backgroud: url('@/static/indexBg.jpg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-bg-left {
|
||||||
|
top: 84rpx;
|
||||||
|
left: -30rpx;
|
||||||
|
width: 300rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
background: radial-gradient(circle, rgba(197, 237, 255, 0.95) 0%, rgba(197, 237, 255, 0) 72%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-bg-right {
|
||||||
|
top: 46rpx;
|
||||||
|
right: -40rpx;
|
||||||
|
width: 360rpx;
|
||||||
|
height: 260rpx;
|
||||||
|
background: radial-gradient(circle, rgba(184, 231, 255, 0.92) 0%, rgba(184, 231, 255, 0) 72%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
padding-left: 28rpx;
|
||||||
|
padding-right: 20rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
backdrop-filter: blur(18rpx);
|
||||||
|
-webkit-backdrop-filter: blur(18rpx);
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(65, 105, 135, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-inner {
|
||||||
|
height: 88rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-left {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-back-btn {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
box-shadow: 0 8rpx 18rpx rgba(115, 164, 196, 0.14);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #101214;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-spacer {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-right-placeholder {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
112
components/u-input/u-input.vue
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<view class="u-input" :style="mergedStyle">
|
||||||
|
<input
|
||||||
|
class="u-input__inner"
|
||||||
|
:type="type"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:placeholder-style="placeholderStyle"
|
||||||
|
:value="inputValue"
|
||||||
|
:maxlength="maxlength"
|
||||||
|
:disabled="disabled"
|
||||||
|
:confirm-type="confirmType"
|
||||||
|
@input="handleInput"
|
||||||
|
@confirm="handleConfirm"
|
||||||
|
@focus="handleFocus"
|
||||||
|
@blur="handleBlur"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'UInput',
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'text'
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
placeholderStyle: {
|
||||||
|
type: String,
|
||||||
|
default: 'color: #c0c4cc;'
|
||||||
|
},
|
||||||
|
maxlength: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 140
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
confirmType: {
|
||||||
|
type: String,
|
||||||
|
default: 'done'
|
||||||
|
},
|
||||||
|
customStyle: {
|
||||||
|
type: [String, Object],
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
inputValue() {
|
||||||
|
return this.modelValue !== '' && this.modelValue !== undefined
|
||||||
|
? this.modelValue
|
||||||
|
: this.value
|
||||||
|
},
|
||||||
|
mergedStyle() {
|
||||||
|
if (typeof this.customStyle === 'string') {
|
||||||
|
return this.customStyle
|
||||||
|
}
|
||||||
|
return this.customStyle || {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleInput(event) {
|
||||||
|
const value = event.detail.value
|
||||||
|
this.$emit('update:modelValue', value)
|
||||||
|
this.$emit('input', value)
|
||||||
|
this.$emit('change', value)
|
||||||
|
},
|
||||||
|
handleConfirm(event) {
|
||||||
|
this.$emit('confirm', event)
|
||||||
|
},
|
||||||
|
handleFocus(event) {
|
||||||
|
this.$emit('focus', event)
|
||||||
|
},
|
||||||
|
handleBlur(event) {
|
||||||
|
this.$emit('blur', event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.u-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.u-input__inner {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
color: inherit;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
20
index.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<script>
|
||||||
|
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||||
|
CSS.supports('top: constant(a)'))
|
||||||
|
document.write(
|
||||||
|
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||||
|
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||||
|
</script>
|
||||||
|
<title></title>
|
||||||
|
<!--preload-links-->
|
||||||
|
<!--app-context-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"><!--app-html--></div>
|
||||||
|
<script type="module" src="/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
27
main.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import App from './App'
|
||||||
|
import uvUI from '@/uni_modules/uv-ui-tools'
|
||||||
|
|
||||||
|
// #ifndef VUE3
|
||||||
|
import Vue from 'vue'
|
||||||
|
import './uni.promisify.adaptor'
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
Vue.use(uvUI)
|
||||||
|
|
||||||
|
App.mpType = 'app'
|
||||||
|
const app = new Vue({
|
||||||
|
...App
|
||||||
|
})
|
||||||
|
app.$mount()
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifdef VUE3
|
||||||
|
import { createSSRApp } from 'vue'
|
||||||
|
export function createApp() {
|
||||||
|
const app = createSSRApp(App)
|
||||||
|
app.use(uvUI)
|
||||||
|
return {
|
||||||
|
app
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
72
manifest.json
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"name" : "tuanwei",
|
||||||
|
"appid" : "__UNI__F254BAB",
|
||||||
|
"description" : "",
|
||||||
|
"versionName" : "1.0.0",
|
||||||
|
"versionCode" : "100",
|
||||||
|
"transformPx" : false,
|
||||||
|
/* 5+App特有相关 */
|
||||||
|
"app-plus" : {
|
||||||
|
"usingComponents" : true,
|
||||||
|
"nvueStyleCompiler" : "uni-app",
|
||||||
|
"compilerVersion" : 3,
|
||||||
|
"splashscreen" : {
|
||||||
|
"alwaysShowBeforeRender" : true,
|
||||||
|
"waiting" : true,
|
||||||
|
"autoclose" : true,
|
||||||
|
"delay" : 0
|
||||||
|
},
|
||||||
|
/* 模块配置 */
|
||||||
|
"modules" : {},
|
||||||
|
/* 应用发布信息 */
|
||||||
|
"distribute" : {
|
||||||
|
/* android打包配置 */
|
||||||
|
"android" : {
|
||||||
|
"permissions" : [
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
/* ios打包配置 */
|
||||||
|
"ios" : {},
|
||||||
|
/* SDK配置 */
|
||||||
|
"sdkConfigs" : {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/* 快应用特有相关 */
|
||||||
|
"quickapp" : {},
|
||||||
|
/* 小程序特有相关 */
|
||||||
|
"mp-weixin" : {
|
||||||
|
"appid" : "wxf7ee9cd5801bf7a6",
|
||||||
|
"setting" : {
|
||||||
|
"urlCheck" : false
|
||||||
|
},
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-alipay" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-baidu" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"mp-toutiao" : {
|
||||||
|
"usingComponents" : true
|
||||||
|
},
|
||||||
|
"uniStatistics" : {
|
||||||
|
"enable" : false
|
||||||
|
},
|
||||||
|
"vueVersion" : "3"
|
||||||
|
}
|
||||||
373
packageMine/declare.vue
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<scroll-view scroll-y class="page-scroll">
|
||||||
|
<common-hero title="我的申报" :use-image-bg="true"></common-hero>
|
||||||
|
<view class="hero-card">
|
||||||
|
<view class="hero-title">我的申报</view>
|
||||||
|
<view class="hero-desc">汇总本地草稿、已提交记录和当前开放的申报项目。</view>
|
||||||
|
<view class="hero-stats">
|
||||||
|
<view class="stat-item">
|
||||||
|
<view class="stat-value">{{ localRecords.length }}</view>
|
||||||
|
<view class="stat-label">本地记录</view>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<view class="stat-value">{{ submittedCount }}</view>
|
||||||
|
<view class="stat-label">已提交</view>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<view class="stat-value">{{ openDeclareList.length }}</view>
|
||||||
|
<view class="stat-label">开放项目</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section-card">
|
||||||
|
<view class="section-head">
|
||||||
|
<view class="section-title">本地申报记录</view>
|
||||||
|
<view class="section-action" @tap="loadData()">刷新</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="!localRecords.length" class="empty-block">
|
||||||
|
当前还没有本地草稿或提交记录,可先前往申报页填写。
|
||||||
|
</view>
|
||||||
|
<view v-else class="card-list">
|
||||||
|
<view v-for="item in localRecords" :key="item.key" class="data-card">
|
||||||
|
<view class="card-top">
|
||||||
|
<view class="card-main">
|
||||||
|
<view class="card-title">{{ item.title }}</view>
|
||||||
|
<view class="card-subtitle">{{ item.year || '未设置年度' }} · {{ item.group }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="status-tag" :class="item.status === '已提交' ? 'status-tag--success' : 'status-tag--warning'">
|
||||||
|
{{ item.status }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-row">保存时间:{{ item.savedAt || '-' }}</view>
|
||||||
|
<view class="card-actions">
|
||||||
|
<view class="card-action" @tap="continueEdit(item)">继续编辑</view>
|
||||||
|
<view class="card-action" @tap="copyText(item.title)">复制标题</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section-card">
|
||||||
|
<view class="section-head">
|
||||||
|
<view class="section-title">当前开放项目</view>
|
||||||
|
<view class="section-action" @tap="goDeclareList">全部查看</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="loading" class="empty-block">正在加载开放项目...</view>
|
||||||
|
<view v-else-if="errorText" class="empty-block empty-block--error">{{ errorText }}</view>
|
||||||
|
<view v-else-if="!openDeclareList.length" class="empty-block">当前暂无开放的申报项目。</view>
|
||||||
|
<view v-else class="card-list">
|
||||||
|
<view v-for="item in openDeclareList" :key="item.id || `${item.module}-${item.year}`" class="data-card">
|
||||||
|
<view class="card-top">
|
||||||
|
<view class="card-main">
|
||||||
|
<view class="card-title">{{ getDeclareTitle(item) }}</view>
|
||||||
|
<view class="card-subtitle">{{ getDeclareGroup(item.module) }} · {{ item.year || '未设置年度' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="status-tag status-tag--default">可申报</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-row">时间范围:{{ formatDateTime(item.startTime) }} 至 {{ formatDateTime(item.endTime) }}</view>
|
||||||
|
<view class="card-actions">
|
||||||
|
<view class="card-action" @tap="goApply(item)">进入申报</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CommonHero from '../components/common-hero/common-hero.vue'
|
||||||
|
import {
|
||||||
|
formatDateTime,
|
||||||
|
getLocalDeclareRecords,
|
||||||
|
listAvailableDeclare
|
||||||
|
} from '../pages/mine/service'
|
||||||
|
import { getFormConfig, getModuleMeta } from '../utils/gxmu/config'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CommonHero
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
errorText: '',
|
||||||
|
localRecords: [],
|
||||||
|
openDeclareList: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
submittedCount() {
|
||||||
|
return this.localRecords.filter((item) => item.status === '已提交').length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
this.loadData()
|
||||||
|
},
|
||||||
|
onPullDownRefresh() {
|
||||||
|
this.loadData(true)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatDateTime,
|
||||||
|
getDeclareTitle(item) {
|
||||||
|
return String(item.title || '').trim() || this.getDeclareGroup(item.module)
|
||||||
|
},
|
||||||
|
getDeclareGroup(module) {
|
||||||
|
const meta = getModuleMeta(module)
|
||||||
|
return (meta && meta.title) || module || '申报项目'
|
||||||
|
},
|
||||||
|
async loadData(fromPullDown = false) {
|
||||||
|
this.localRecords = getLocalDeclareRecords()
|
||||||
|
this.loading = true
|
||||||
|
this.errorText = ''
|
||||||
|
try {
|
||||||
|
const result = await listAvailableDeclare({ usable: true })
|
||||||
|
const list = Array.isArray(result) ? result : []
|
||||||
|
this.openDeclareList = list.sort((left, right) => Number(right.year || 0) - Number(left.year || 0))
|
||||||
|
} catch (error) {
|
||||||
|
this.errorText = (error && error.message) || '开放项目加载失败'
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
if (fromPullDown) {
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
continueEdit(item) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: item.route
|
||||||
|
})
|
||||||
|
},
|
||||||
|
copyText(value) {
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: String(value || ''),
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '已复制',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goDeclareList() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/gxmu/index'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goApply(item) {
|
||||||
|
if (!item.module || !getFormConfig(item.module)) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '当前申报项目在小程序端暂未配置',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const moduleMeta = getModuleMeta(item.module)
|
||||||
|
const title = String(item.title || '').trim() || ((moduleMeta && moduleMeta.title) || '申报表')
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/gxmu/form?code=${encodeURIComponent(item.module)}&year=${encodeURIComponent(item.year || '')}&declareTitle=${encodeURIComponent(title)}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top center, rgba(20, 150, 242, 0.1), transparent 30%),
|
||||||
|
linear-gradient(180deg, #f5fbff 0%, #eef7ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-scroll {
|
||||||
|
height: 100vh;
|
||||||
|
padding: 24rpx 24rpx 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card,
|
||||||
|
.section-card,
|
||||||
|
.data-card {
|
||||||
|
border-radius: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
padding: 34rpx 36rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
box-shadow: 0 4rpx 12rpx 0 rgba(174, 174, 174, 0.4);
|
||||||
|
backdrop-filter: blur(24rpx);
|
||||||
|
-webkit-backdrop-filter: blur(24rpx);
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
padding-left: 10rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: #18a0f7;
|
||||||
|
border-left: 5px solid #0f94ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-desc {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #7f7f7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card,
|
||||||
|
.data-card {
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
border: 1rpx solid rgba(20, 150, 242, 0.08);
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(34, 94, 142, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
padding: 18rpx 16rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-action {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #1496f2;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-block {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
padding: 26rpx 22rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #faf5f2;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.75;
|
||||||
|
color: #7b726d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-block--error {
|
||||||
|
color: #0f7dd1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-card {
|
||||||
|
padding: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #22272f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #8b817c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-row {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
padding: 18rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: #faf7f2;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.75;
|
||||||
|
color: #5f564f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
padding: 10rpx 16rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--success {
|
||||||
|
background: #eef9f1;
|
||||||
|
color: #1f8f49;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--warning {
|
||||||
|
background: #fff8e6;
|
||||||
|
color: #ad7b00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--default {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #8b817c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-action {
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(20, 150, 242, 0.08);
|
||||||
|
color: #1496f2;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
441
packageMine/manuscripts.vue
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<scroll-view scroll-y class="page-scroll">
|
||||||
|
<common-hero title="我的稿件" :use-image-bg="true"></common-hero>
|
||||||
|
<view class="hero-card">
|
||||||
|
<view class="hero-title">我的稿件</view>
|
||||||
|
<view class="hero-desc">汇总当前账号创建或参与的稿件记录,可查看状态和内容摘要。</view>
|
||||||
|
<view class="hero-stats">
|
||||||
|
<view class="stat-item">
|
||||||
|
<view class="stat-value">{{ manuscripts.length }}</view>
|
||||||
|
<view class="stat-label">稿件总数</view>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<view class="stat-value">{{ publishedCount }}</view>
|
||||||
|
<view class="stat-label">已通过/发布</view>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<view class="stat-value">{{ pendingCount }}</view>
|
||||||
|
<view class="stat-label">待处理</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="toolbar-card">
|
||||||
|
<uv-input
|
||||||
|
v-model="keyword"
|
||||||
|
class="search-input"
|
||||||
|
placeholder="搜索稿件标题或内容"
|
||||||
|
placeholder-style="color: #c0c4cc;"
|
||||||
|
:maxlength="-1"
|
||||||
|
/>
|
||||||
|
<view class="toolbar-actions">
|
||||||
|
<view class="toolbar-btn" @tap="loadData()">刷新</view>
|
||||||
|
<view class="toolbar-btn toolbar-btn--primary" @tap="goCreate">去创作</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading" class="state-card">
|
||||||
|
<view class="state-title">正在加载稿件...</view>
|
||||||
|
<view class="state-desc">请稍候,正在同步当前账号的稿件记录。</view>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="errorText" class="state-card state-card--error">
|
||||||
|
<view class="state-title">加载失败</view>
|
||||||
|
<view class="state-desc">{{ errorText }}</view>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="!filteredList.length" class="state-card">
|
||||||
|
<view class="state-title">暂无稿件</view>
|
||||||
|
<view class="state-desc">当前账号还没有可展示的稿件,可前往文稿工作台开始生成。</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="card-list">
|
||||||
|
<view v-for="item in filteredList" :key="item.id || item.title" class="data-card">
|
||||||
|
<view class="card-top">
|
||||||
|
<view class="card-main">
|
||||||
|
<view class="card-title">{{ item.title || '未命名稿件' }}</view>
|
||||||
|
<view class="card-subtitle">
|
||||||
|
创建时间:{{ formatDateTime(item.createTime) }} · 更新时间:{{ formatDateTime(item.updateTime) }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="status-tag" :class="`status-tag--${getStatusMeta(item).tone}`">
|
||||||
|
{{ getStatusMeta(item).text }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="item.cover" class="cover-wrap">
|
||||||
|
<image class="cover-image" :src="item.cover" mode="aspectFill" />
|
||||||
|
</view>
|
||||||
|
<view class="content-preview">{{ getContentPreview(item.content) }}</view>
|
||||||
|
<view class="card-actions">
|
||||||
|
<view class="card-action" @tap="toggleExpand(item)">{{ expandedId === item.id ? '收起内容' : '查看内容' }}</view>
|
||||||
|
<view class="card-action" @tap="copyTitle(item.title)">复制标题</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="expandedId === item.id" class="content-detail">{{ item.content || '暂无正文内容' }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CommonHero from '../components/common-hero/common-hero.vue'
|
||||||
|
import {
|
||||||
|
formatDateTime,
|
||||||
|
getCurrentUser,
|
||||||
|
getManuscriptStatusMeta,
|
||||||
|
listManuscript
|
||||||
|
} from '../pages/mine/service'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CommonHero
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
errorText: '',
|
||||||
|
keyword: '',
|
||||||
|
expandedId: null,
|
||||||
|
manuscripts: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filteredList() {
|
||||||
|
const keyword = String(this.keyword || '').trim().toLowerCase()
|
||||||
|
if (!keyword) {
|
||||||
|
return this.manuscripts
|
||||||
|
}
|
||||||
|
return this.manuscripts.filter((item) => {
|
||||||
|
return (
|
||||||
|
String(item.title || '').toLowerCase().includes(keyword) ||
|
||||||
|
String(item.content || '').toLowerCase().includes(keyword)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
publishedCount() {
|
||||||
|
return this.manuscripts.filter((item) => {
|
||||||
|
const text = this.getStatusMeta(item).text
|
||||||
|
return text === '通过' || text === '已发布'
|
||||||
|
}).length
|
||||||
|
},
|
||||||
|
pendingCount() {
|
||||||
|
return this.manuscripts.length - this.publishedCount
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
this.loadData()
|
||||||
|
},
|
||||||
|
onPullDownRefresh() {
|
||||||
|
this.loadData(true)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatDateTime,
|
||||||
|
getStatusMeta(item) {
|
||||||
|
return getManuscriptStatusMeta(item)
|
||||||
|
},
|
||||||
|
getContentPreview(content) {
|
||||||
|
const text = String(content || '').replace(/\s+/g, ' ').trim()
|
||||||
|
return text ? `${text.slice(0, 90)}${text.length > 90 ? '...' : ''}` : '暂无内容摘要'
|
||||||
|
},
|
||||||
|
async loadData(fromPullDown = false) {
|
||||||
|
this.loading = true
|
||||||
|
this.errorText = ''
|
||||||
|
try {
|
||||||
|
const user = getCurrentUser()
|
||||||
|
const result = await listManuscript()
|
||||||
|
const list = Array.isArray(result) ? result : []
|
||||||
|
this.manuscripts = list
|
||||||
|
.filter((item) => !user.userId || Number(item.userId) === Number(user.userId))
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftTime = new Date(String((left.updateTime || left.createTime || '')).replace(/-/g, '/')).getTime()
|
||||||
|
const rightTime = new Date(String((right.updateTime || right.createTime || '')).replace(/-/g, '/')).getTime()
|
||||||
|
return (Number.isNaN(rightTime) ? 0 : rightTime) - (Number.isNaN(leftTime) ? 0 : leftTime)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
this.errorText = (error && error.message) || '稿件加载失败'
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
if (fromPullDown) {
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleExpand(item) {
|
||||||
|
this.expandedId = this.expandedId === item.id ? null : item.id
|
||||||
|
},
|
||||||
|
copyTitle(title) {
|
||||||
|
if (!title) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '暂无标题可复制',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: title,
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '标题已复制',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goCreate() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/manuscript/index'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top center, rgba(240, 122, 61, 0.1), transparent 30%),
|
||||||
|
linear-gradient(180deg, #fff8f2 0%, #f8f2ec 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-scroll {
|
||||||
|
height: 100vh;
|
||||||
|
padding: 24rpx 24rpx 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card,
|
||||||
|
.toolbar-card,
|
||||||
|
.state-card,
|
||||||
|
.data-card {
|
||||||
|
border-radius: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
padding: 34rpx 36rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
box-shadow: 0 4rpx 12rpx 0 rgba(174, 174, 174, 0.4);
|
||||||
|
backdrop-filter: blur(24rpx);
|
||||||
|
-webkit-backdrop-filter: blur(24rpx);
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
padding-left: 10rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: #18a0f7;
|
||||||
|
border-left: 5px solid #0f94ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-desc {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #7f7f7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-card,
|
||||||
|
.state-card,
|
||||||
|
.data-card {
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
border: 1rpx solid rgba(240, 122, 61, 0.08);
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(76, 49, 35, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
padding: 18rpx 16rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: rgba(255, 250, 245, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-card {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.search-input.uv-input) {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 84rpx;
|
||||||
|
padding: 0 22rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
background: #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.search-input.uv-input .uv-input__content__field-wrapper__field) {
|
||||||
|
height: 84rpx;
|
||||||
|
min-height: 84rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 84rpx;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn {
|
||||||
|
height: 84rpx;
|
||||||
|
line-height: 84rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: #edf6ff;
|
||||||
|
color: #0f7dd1;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn--primary {
|
||||||
|
background: linear-gradient(135deg, #0f7dd1 0%, #1496f2 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding: 28rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card--error {
|
||||||
|
border-color: rgba(20, 150, 242, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #20252c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-desc {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.75;
|
||||||
|
color: #786e69;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-card {
|
||||||
|
padding: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #22272f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #8b817c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
padding: 10rpx 16rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--success {
|
||||||
|
background: #eef9f1;
|
||||||
|
color: #1f8f49;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--danger {
|
||||||
|
background: #fff1f0;
|
||||||
|
color: #d4380d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--warning {
|
||||||
|
background: #fff8e6;
|
||||||
|
color: #ad7b00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--default {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #8b817c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-wrap {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 260rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-preview,
|
||||||
|
.content-detail {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #5d544f;
|
||||||
|
word-break: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-detail {
|
||||||
|
padding: 20rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: #fbf7f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-action {
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(240, 122, 61, 0.08);
|
||||||
|
color: #c8641f;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
932
packageMine/profile.vue
Normal file
@@ -0,0 +1,932 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<scroll-view scroll-y class="page-scroll">
|
||||||
|
<common-hero title="用户资料" :use-image-bg="true" :reduce-top="24"></common-hero>
|
||||||
|
|
||||||
|
<view class="section-card">
|
||||||
|
<view class="section-title">基础信息</view>
|
||||||
|
<view class="info-list">
|
||||||
|
<!-- <view class="info-item info-item--form">-->
|
||||||
|
<!-- <view class="info-main">-->
|
||||||
|
<!-- <view class="info-label">登录账号</view>-->
|
||||||
|
<!-- <view class="info-value">{{ profile.username || '-' }}</view>-->
|
||||||
|
<!-- </view>-->
|
||||||
|
<!-- </view>-->
|
||||||
|
<view class="info-item info-item--form">
|
||||||
|
<view class="info-main">
|
||||||
|
<view class="info-label">姓名</view>
|
||||||
|
<uv-input
|
||||||
|
class="info-input"
|
||||||
|
:value="editingRealName"
|
||||||
|
maxlength="20"
|
||||||
|
placeholder="请输入姓名"
|
||||||
|
placeholder-style="color: #b2a7a1;"
|
||||||
|
border="none"
|
||||||
|
@input="handleRealNameInput"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-item info-item--form">
|
||||||
|
<view class="info-main">
|
||||||
|
<view class="info-label">昵称</view>
|
||||||
|
<uv-input
|
||||||
|
class="info-input"
|
||||||
|
type="nickname"
|
||||||
|
:value="editingNickname"
|
||||||
|
maxlength="20"
|
||||||
|
placeholder="请输入昵称"
|
||||||
|
placeholder-style="color: #b2a7a1;"
|
||||||
|
border="none"
|
||||||
|
@input="handleNicknameInput"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-item info-item--form">
|
||||||
|
<view class="info-main">
|
||||||
|
<view class="info-label">性别</view>
|
||||||
|
<picker
|
||||||
|
mode="selector"
|
||||||
|
:range="genderOptions"
|
||||||
|
:value="genderIndex"
|
||||||
|
@change="handleGenderChange"
|
||||||
|
>
|
||||||
|
<view class="info-input info-input--picker">
|
||||||
|
{{ editingGender || '请选择性别' }}
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-item info-item--form">
|
||||||
|
<view class="info-main">
|
||||||
|
<view class="info-label">所属组织机构</view>
|
||||||
|
<view class="info-input info-input--picker" @tap="openOrganizationPicker">
|
||||||
|
{{ editingOrganizationName || '请选择所属组织机构' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-item info-item--form">
|
||||||
|
<view class="info-main">
|
||||||
|
<view class="info-label">手机号码</view>
|
||||||
|
<view class="info-value">{{ profile.phone || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="profile.phone" class="info-action" @tap="copyText(profile.phone)">复制</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-actions">
|
||||||
|
<view class="nickname-btn nickname-btn--primary" @tap="saveProfileBasic">保存基础信息</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view v-if="organizationPickerVisible" class="organization-mask" @tap="closeOrganizationPicker">
|
||||||
|
<view class="organization-panel" @tap.stop>
|
||||||
|
<view class="organization-head">
|
||||||
|
<view class="organization-title">选择所属组织机构</view>
|
||||||
|
<view class="organization-close" @tap="closeOrganizationPicker">关闭</view>
|
||||||
|
</view>
|
||||||
|
<input
|
||||||
|
v-model="organizationKeyword"
|
||||||
|
class="organization-search"
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索组织机构"
|
||||||
|
placeholder-style="color: #b2a7a1;"
|
||||||
|
/>
|
||||||
|
<scroll-view scroll-y class="organization-scroll">
|
||||||
|
<view v-if="filteredOrganizationTree.length" class="organization-tree">
|
||||||
|
<view
|
||||||
|
v-for="item in filteredOrganizationTree"
|
||||||
|
:key="item.value"
|
||||||
|
class="organization-tree-node"
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
class="organization-item"
|
||||||
|
:class="{ 'organization-item--active': String(item.value) === String(editingOrganizationId) }"
|
||||||
|
:style="{ paddingLeft: `${20 + item.level * 28}rpx` }"
|
||||||
|
>
|
||||||
|
<view class="organization-item-main" @tap="selectOrganization(item)">
|
||||||
|
<view class="organization-item-name">{{ item.rawLabel || item.text }}</view>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="item.children && item.children.length"
|
||||||
|
class="organization-item-toggle"
|
||||||
|
@tap.stop="toggleOrganizationNode(item.value)"
|
||||||
|
>
|
||||||
|
{{ isOrganizationExpanded(item.value) ? '收起' : '展开' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="organization-empty">没有匹配的组织机构</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CommonHero from '../components/common-hero/common-hero.vue'
|
||||||
|
import {
|
||||||
|
getCurrentUser,
|
||||||
|
getLocalProfileOverride,
|
||||||
|
getUserProfile,
|
||||||
|
listOrganizations,
|
||||||
|
saveLocalProfileOverride,
|
||||||
|
updateUserProfileData
|
||||||
|
} from '../pages/mine/service'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CommonHero
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
editingNickname: '',
|
||||||
|
editingRealName: '',
|
||||||
|
editingGender: '',
|
||||||
|
editingOrganizationId: '',
|
||||||
|
editingOrganizationName: '',
|
||||||
|
genderOptions: ['男', '女', '未知'],
|
||||||
|
organizationTree: [],
|
||||||
|
organizationPickerVisible: false,
|
||||||
|
organizationKeyword: '',
|
||||||
|
organizationExpandedMap: {},
|
||||||
|
profile: {
|
||||||
|
userId: '',
|
||||||
|
nickname: '',
|
||||||
|
realName: '',
|
||||||
|
username: '',
|
||||||
|
avatar: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
organizationId: '',
|
||||||
|
organizationName: '',
|
||||||
|
tenantName: '',
|
||||||
|
merchantName: '',
|
||||||
|
sexName: '',
|
||||||
|
sex: '',
|
||||||
|
address: '',
|
||||||
|
province: '',
|
||||||
|
city: '',
|
||||||
|
region: '',
|
||||||
|
introduction: '',
|
||||||
|
roles: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayName() {
|
||||||
|
return this.profile.nickname || this.profile.realName || this.profile.username || '未命名用户'
|
||||||
|
},
|
||||||
|
roleText() {
|
||||||
|
const roles = Array.isArray(this.profile.roles) ? this.profile.roles : []
|
||||||
|
if (roles.length) {
|
||||||
|
return roles.map((item) => item.roleName).filter(Boolean).join(' / ')
|
||||||
|
}
|
||||||
|
return '暂无角色信息'
|
||||||
|
},
|
||||||
|
organizationText() {
|
||||||
|
return this.profile.organizationName || this.profile.merchantName || this.profile.tenantName || '暂无组织信息'
|
||||||
|
},
|
||||||
|
avatarText() {
|
||||||
|
return (this.displayName || '我').slice(0, 2)
|
||||||
|
},
|
||||||
|
genderIndex() {
|
||||||
|
const index = this.genderOptions.indexOf(this.editingGender)
|
||||||
|
return index > -1 ? index : 0
|
||||||
|
},
|
||||||
|
filteredOrganizationTree() {
|
||||||
|
const keyword = String(this.organizationKeyword || '').trim().toLowerCase()
|
||||||
|
const source = Array.isArray(this.organizationTree) ? this.organizationTree : []
|
||||||
|
if (!keyword) {
|
||||||
|
return this.flattenVisibleOrganizationTree(source)
|
||||||
|
}
|
||||||
|
const filteredTree = this.filterOrganizationTree(source, keyword)
|
||||||
|
return this.flattenVisibleOrganizationTree(filteredTree, true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
this.loadProfile()
|
||||||
|
this.loadOrganizationOptions()
|
||||||
|
},
|
||||||
|
onPullDownRefresh() {
|
||||||
|
this.loadProfile(true)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getProfileUserId() {
|
||||||
|
return Number(this.profile.userId || getCurrentUser().userId || 0) || ''
|
||||||
|
},
|
||||||
|
applyLocalProfileOverride() {
|
||||||
|
const override = getLocalProfileOverride(this.getProfileUserId())
|
||||||
|
if (override.nickname !== undefined) {
|
||||||
|
this.profile = {
|
||||||
|
...this.profile,
|
||||||
|
nickname: String(override.nickname || '').trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (override.realName !== undefined) {
|
||||||
|
this.profile = {
|
||||||
|
...this.profile,
|
||||||
|
realName: String(override.realName || '').trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (override.sex !== undefined || override.sexName !== undefined) {
|
||||||
|
const nextGender = String(override.sexName || override.sex || '').trim()
|
||||||
|
this.profile = {
|
||||||
|
...this.profile,
|
||||||
|
sex: nextGender,
|
||||||
|
sexName: nextGender
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (override.organizationId !== undefined || override.organizationName !== undefined) {
|
||||||
|
this.profile = {
|
||||||
|
...this.profile,
|
||||||
|
organizationId: override.organizationId || '',
|
||||||
|
organizationName: String(override.organizationName || '').trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.editingNickname = this.profile.nickname || ''
|
||||||
|
this.editingRealName = this.profile.realName || ''
|
||||||
|
this.editingGender = this.profile.sexName || this.profile.sex || ''
|
||||||
|
this.editingOrganizationId = this.profile.organizationId || ''
|
||||||
|
this.editingOrganizationName = this.profile.organizationName || ''
|
||||||
|
},
|
||||||
|
async loadProfile(fromPullDown = false) {
|
||||||
|
try {
|
||||||
|
const tokenUser = getCurrentUser()
|
||||||
|
this.profile = {
|
||||||
|
...this.profile,
|
||||||
|
...tokenUser
|
||||||
|
}
|
||||||
|
const result = await getUserProfile()
|
||||||
|
this.profile = {
|
||||||
|
...this.profile,
|
||||||
|
...(result || {})
|
||||||
|
}
|
||||||
|
this.applyLocalProfileOverride()
|
||||||
|
} catch (error) {
|
||||||
|
this.applyLocalProfileOverride()
|
||||||
|
if (fromPullDown) {
|
||||||
|
uni.showToast({
|
||||||
|
title: (error && error.message) || '用户资料加载失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (fromPullDown) {
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadOrganizationOptions() {
|
||||||
|
try {
|
||||||
|
const organizationList = await listOrganizations()
|
||||||
|
if (!Array.isArray(organizationList) || !organizationList.length) {
|
||||||
|
this.organizationTree = []
|
||||||
|
this.organizationExpandedMap = {}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.organizationTree = this.buildOrganizationTree(organizationList)
|
||||||
|
this.organizationExpandedMap = this.buildOrganizationExpandedMap(this.organizationTree, this.editingOrganizationId)
|
||||||
|
if (this.editingOrganizationId && !this.editingOrganizationName) {
|
||||||
|
const matched = this.findOrganizationNodeByValue(this.organizationTree, this.editingOrganizationId)
|
||||||
|
if (matched) {
|
||||||
|
this.editingOrganizationName = this.buildOrganizationLabel(this.organizationTree, this.editingOrganizationId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.organizationTree = []
|
||||||
|
this.organizationExpandedMap = {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
normalizeOrganizationNode(item) {
|
||||||
|
return {
|
||||||
|
text: item.organizationName || item.name || item.label || '',
|
||||||
|
value: Number(item.organizationId || item.id || item.value || 0) || String(item.organizationId || item.id || item.value || ''),
|
||||||
|
rawLabel: item.organizationName || item.name || item.label || '',
|
||||||
|
parentId: Number(item.parentId || item.parentID || item.pid || 0) || 0,
|
||||||
|
children: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildOrganizationTree(list) {
|
||||||
|
const source = Array.isArray(list) ? list : []
|
||||||
|
const nodeMap = {}
|
||||||
|
source.forEach((item) => {
|
||||||
|
const normalized = this.normalizeOrganizationNode(item)
|
||||||
|
if (normalized.text && normalized.value !== '') {
|
||||||
|
nodeMap[String(normalized.value)] = normalized
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const roots = []
|
||||||
|
Object.keys(nodeMap).forEach((key) => {
|
||||||
|
const current = nodeMap[key]
|
||||||
|
const parentKey = String(current.parentId || 0)
|
||||||
|
if (current.parentId && nodeMap[parentKey]) {
|
||||||
|
nodeMap[parentKey].children.push(current)
|
||||||
|
} else {
|
||||||
|
roots.push(current)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return roots
|
||||||
|
},
|
||||||
|
findOrganizationNodeByValue(list, targetValue) {
|
||||||
|
const source = Array.isArray(list) ? list : []
|
||||||
|
const normalizedTarget = String(targetValue || '')
|
||||||
|
for (let index = 0; index < source.length; index += 1) {
|
||||||
|
const item = source[index]
|
||||||
|
if (String(item.value) === normalizedTarget) {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
if (Array.isArray(item.children) && item.children.length) {
|
||||||
|
const matched = this.findOrganizationNodeByValue(item.children, targetValue)
|
||||||
|
if (matched) {
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
findOrganizationPathNodes(list, targetValue, path = []) {
|
||||||
|
const source = Array.isArray(list) ? list : []
|
||||||
|
const normalizedTarget = String(targetValue || '')
|
||||||
|
for (let index = 0; index < source.length; index += 1) {
|
||||||
|
const item = source[index]
|
||||||
|
const nextPath = path.concat(item)
|
||||||
|
if (String(item.value) === normalizedTarget) {
|
||||||
|
return nextPath
|
||||||
|
}
|
||||||
|
if (Array.isArray(item.children) && item.children.length) {
|
||||||
|
const matched = this.findOrganizationPathNodes(item.children, targetValue, nextPath)
|
||||||
|
if (matched) {
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
buildOrganizationLabel(list, targetValue) {
|
||||||
|
const pathNodes = this.findOrganizationPathNodes(list, targetValue) || []
|
||||||
|
return pathNodes.map((item) => item.rawLabel || item.text).filter(Boolean).join(' / ')
|
||||||
|
},
|
||||||
|
buildOrganizationExpandedMap(list, targetValue) {
|
||||||
|
const expandedMap = {}
|
||||||
|
const pathNodes = this.findOrganizationPathNodes(list, targetValue) || []
|
||||||
|
pathNodes.forEach((item) => {
|
||||||
|
expandedMap[String(item.value)] = true
|
||||||
|
})
|
||||||
|
return expandedMap
|
||||||
|
},
|
||||||
|
filterOrganizationTree(list, keyword) {
|
||||||
|
return (Array.isArray(list) ? list : [])
|
||||||
|
.map((item) => {
|
||||||
|
const children = this.filterOrganizationTree(item.children || [], keyword)
|
||||||
|
const text = String(item.rawLabel || item.text || '').toLowerCase()
|
||||||
|
if (text.includes(keyword) || children.length) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
},
|
||||||
|
flattenVisibleOrganizationTree(list, forceExpand = false, level = 0) {
|
||||||
|
const result = []
|
||||||
|
;(Array.isArray(list) ? list : []).forEach((item) => {
|
||||||
|
result.push({
|
||||||
|
...item,
|
||||||
|
level
|
||||||
|
})
|
||||||
|
const shouldExpand = forceExpand || this.isOrganizationExpanded(item.value)
|
||||||
|
if (shouldExpand && Array.isArray(item.children) && item.children.length) {
|
||||||
|
result.push(...this.flattenVisibleOrganizationTree(item.children, forceExpand, level + 1))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
handleNicknameInput(event) {
|
||||||
|
const value = typeof event === 'string' ? event : (event.detail && event.detail.value) || ''
|
||||||
|
this.editingNickname = String(value || '')
|
||||||
|
},
|
||||||
|
handleRealNameInput(event) {
|
||||||
|
const value = typeof event === 'string' ? event : (event.detail && event.detail.value) || ''
|
||||||
|
this.editingRealName = String(value || '')
|
||||||
|
},
|
||||||
|
handleGenderChange(event) {
|
||||||
|
const index = Number((event.detail && event.detail.value) || 0)
|
||||||
|
this.editingGender = this.genderOptions[index] || ''
|
||||||
|
},
|
||||||
|
openOrganizationPicker() {
|
||||||
|
if (!this.organizationTree.length) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '暂无组织机构数据',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.organizationKeyword = ''
|
||||||
|
this.organizationPickerVisible = true
|
||||||
|
},
|
||||||
|
closeOrganizationPicker() {
|
||||||
|
this.organizationPickerVisible = false
|
||||||
|
},
|
||||||
|
isOrganizationExpanded(value) {
|
||||||
|
return !!this.organizationExpandedMap[String(value)]
|
||||||
|
},
|
||||||
|
toggleOrganizationNode(value) {
|
||||||
|
const key = String(value)
|
||||||
|
this.organizationExpandedMap = {
|
||||||
|
...this.organizationExpandedMap,
|
||||||
|
[key]: !this.organizationExpandedMap[key]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectOrganization(item) {
|
||||||
|
this.editingOrganizationId = item.value
|
||||||
|
this.editingOrganizationName = this.buildOrganizationLabel(this.organizationTree, item.value)
|
||||||
|
this.closeOrganizationPicker()
|
||||||
|
},
|
||||||
|
async saveProfileBasic() {
|
||||||
|
const nickname = String(this.editingNickname || '').trim()
|
||||||
|
const realName = String(this.editingRealName || '').trim()
|
||||||
|
const gender = String(this.editingGender || '').trim()
|
||||||
|
const organizationId = this.editingOrganizationId
|
||||||
|
const organizationName = String(this.editingOrganizationName || '').trim()
|
||||||
|
try {
|
||||||
|
await updateUserProfileData({
|
||||||
|
userId: this.getProfileUserId() || undefined,
|
||||||
|
nickname,
|
||||||
|
realName,
|
||||||
|
sex: gender,
|
||||||
|
sexName: gender,
|
||||||
|
organizationId,
|
||||||
|
organizationName
|
||||||
|
})
|
||||||
|
this.profile = {
|
||||||
|
...this.profile,
|
||||||
|
nickname,
|
||||||
|
realName,
|
||||||
|
sex: gender,
|
||||||
|
sexName: gender,
|
||||||
|
organizationId,
|
||||||
|
organizationName
|
||||||
|
}
|
||||||
|
this.editingNickname = nickname
|
||||||
|
this.editingRealName = realName
|
||||||
|
this.editingGender = gender
|
||||||
|
this.editingOrganizationId = organizationId
|
||||||
|
this.editingOrganizationName = organizationName
|
||||||
|
saveLocalProfileOverride(this.getProfileUserId(), {
|
||||||
|
nickname,
|
||||||
|
realName,
|
||||||
|
sex: gender,
|
||||||
|
sexName: gender,
|
||||||
|
organizationId,
|
||||||
|
organizationName
|
||||||
|
})
|
||||||
|
uni.showToast({
|
||||||
|
title: '资料已保存',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({
|
||||||
|
title: (error && error.message) || '保存失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fillWechatNickname() {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
const assignNickname = (userInfo) => {
|
||||||
|
const nickname = String((userInfo && (userInfo.nickName || userInfo.nickname)) || '').trim()
|
||||||
|
if (!nickname) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '未读取到微信昵称',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.editingNickname = nickname
|
||||||
|
uni.showToast({
|
||||||
|
title: '已读取微信昵称',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (typeof uni.getUserProfile === 'function') {
|
||||||
|
uni.getUserProfile({
|
||||||
|
desc: '用于完善用户昵称',
|
||||||
|
success: (res) => {
|
||||||
|
assignNickname(res && res.userInfo)
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '未授权读取微信昵称',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof uni.getUserInfo === 'function') {
|
||||||
|
uni.getUserInfo({
|
||||||
|
success: (res) => {
|
||||||
|
assignNickname(res && res.userInfo)
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '读取微信昵称失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showToast({
|
||||||
|
title: '当前环境不支持读取微信昵称',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
// #endif
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
uni.showToast({
|
||||||
|
title: '仅微信小程序支持读取微信昵称',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
},
|
||||||
|
copyText(value) {
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: String(value || ''),
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '已复制',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goPage(url) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top center, rgba(20, 150, 242, 0.1), transparent 28%),
|
||||||
|
linear-gradient(180deg, #f5fbff 0%, #eef7ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-scroll {
|
||||||
|
height: 100vh;
|
||||||
|
padding: 24rpx 24rpx 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card,
|
||||||
|
.section-card {
|
||||||
|
border-radius: 32rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
border: 1rpx solid rgba(20, 150, 242, 0.08);
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(34, 94, 142, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
padding: 34rpx 30rpx;
|
||||||
|
background: linear-gradient(145deg, #0d6fba 0%, #1496f2 58%, #5bc2ff 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-image,
|
||||||
|
.avatar-text {
|
||||||
|
width: 112rpx;
|
||||||
|
height: 112rpx;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
margin-left: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-name {
|
||||||
|
font-size: 38rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-role,
|
||||||
|
.hero-org {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: rgba(255, 255, 255, 0.84);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
padding: 20rpx 16rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 20rpx 18rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #faf5f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #8b817c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 25rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #2b3037;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-action {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #1496f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item--form {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.info-input.uv-input) {
|
||||||
|
width: 100%;
|
||||||
|
height: 92rpx;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
padding: 0 24rpx !important;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #ffffff !important;
|
||||||
|
border: 1rpx solid #eadfd7 !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.info-input.uv-input .uv-input__content__field-wrapper__field) {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #2b3037;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-input--picker {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 92rpx;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1rpx solid #eadfd7;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #2b3037;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organization-mask {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 99;
|
||||||
|
background: rgba(44, 28, 23, 0.28);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organization-panel {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 76vh;
|
||||||
|
padding: 28rpx 24rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||||
|
border-radius: 32rpx 32rpx 0 0;
|
||||||
|
background: #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organization-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organization-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organization-close {
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
background: rgba(20, 150, 242, 0.08);
|
||||||
|
color: #1496f2;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organization-search {
|
||||||
|
width: 100%;
|
||||||
|
height: 88rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1rpx solid #eadfd7;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #2b3037;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organization-scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organization-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 10rpx 20rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
//background: #faf5f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organization-item + .organization-item {
|
||||||
|
margin-top: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organization-item--active {
|
||||||
|
background: rgba(20, 150, 242, 0.08);
|
||||||
|
border: 1rpx solid rgba(20, 150, 242, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.organization-item-name {
|
||||||
|
font-size: 25rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #2b3037;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organization-item-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organization-item-toggle {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(20, 150, 242, 0.08);
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #1496f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.organization-empty {
|
||||||
|
padding: 56rpx 0 32rpx;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #8b817c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-actions {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-btn {
|
||||||
|
height: 80rpx;
|
||||||
|
line-height: 80rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-btn--ghost {
|
||||||
|
background: #eef6ff;
|
||||||
|
color: #1496f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-btn--primary {
|
||||||
|
background: linear-gradient(135deg, #0d6fba 0%, #1496f2 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-tip {
|
||||||
|
margin-top: 14rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #8b817c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bio-text {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #746a65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-card {
|
||||||
|
padding: 22rpx 18rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #fbf6f1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: rgba(20, 150, 242, 0.1);
|
||||||
|
color: #1496f2;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-title {
|
||||||
|
margin-top: 14rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2b3037;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
362
packageMine/qingma.vue
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<scroll-view scroll-y class="page-scroll">
|
||||||
|
<common-hero title="我的青马" :use-image-bg="true"></common-hero>
|
||||||
|
<view class="hero-card">
|
||||||
|
<view class="hero-title">我的“青马”</view>
|
||||||
|
<view class="hero-desc">查看青马工程报名记录、培养信息与当前审核状态。</view>
|
||||||
|
<view class="hero-stats">
|
||||||
|
<view class="stat-item">
|
||||||
|
<view class="stat-value">{{ records.length }}</view>
|
||||||
|
<view class="stat-label">记录总数</view>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<view class="stat-value">{{ approvedCount }}</view>
|
||||||
|
<view class="stat-label">已通过</view>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<view class="stat-value">{{ pendingCount }}</view>
|
||||||
|
<view class="stat-label">待跟进</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading" class="state-card">
|
||||||
|
<view class="state-title">正在加载青马数据...</view>
|
||||||
|
<view class="state-desc">请稍候,正在同步你的青马工程记录。</view>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="errorText" class="state-card state-card--error">
|
||||||
|
<view class="state-title">加载失败</view>
|
||||||
|
<view class="state-desc">{{ errorText }}</view>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="!records.length" class="state-card">
|
||||||
|
<view class="state-title">暂未查询到青马记录</view>
|
||||||
|
<view class="state-desc">当前账号还没有青马工程报名或培养数据,可前往申报页查看开放项目。</view>
|
||||||
|
<view class="state-action" @tap="goApplyList">查看申报列表</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="card-list">
|
||||||
|
<view v-for="item in records" :key="item.id || `${item.name}-${item.year}`" class="data-card">
|
||||||
|
<view class="card-top">
|
||||||
|
<view class="card-main">
|
||||||
|
<view class="card-title">{{ item.name || '未命名学员' }}</view>
|
||||||
|
<view class="card-subtitle">{{ item.year || '未设置年度' }} · 青马工程</view>
|
||||||
|
</view>
|
||||||
|
<view class="status-tag" :class="`status-tag--${getStatusMeta(item).tone}`">
|
||||||
|
{{ getStatusMeta(item).text }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-list">
|
||||||
|
<view class="meta-item">
|
||||||
|
<view class="meta-label">学校院系</view>
|
||||||
|
<view class="meta-value">{{ item.schoolInfo || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-item">
|
||||||
|
<view class="meta-label">手机号码</view>
|
||||||
|
<view class="meta-value">{{ item.phone || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-item">
|
||||||
|
<view class="meta-label">团学职务</view>
|
||||||
|
<view class="meta-value">{{ item.leaguePosition || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-item">
|
||||||
|
<view class="meta-label">综合成绩</view>
|
||||||
|
<view class="meta-value">{{ item.academicPerformance || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="card-actions">
|
||||||
|
<view class="card-action" @tap="copyValue(item.phone)">复制电话</view>
|
||||||
|
<view class="card-action" @tap="copyValue(item.schoolInfo)">复制院系</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CommonHero from '../components/common-hero/common-hero.vue'
|
||||||
|
import { getReviewStatusMeta, userPageQmgcForm } from '../pages/mine/service'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CommonHero
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
errorText: '',
|
||||||
|
records: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
approvedCount() {
|
||||||
|
return this.records.filter((item) => this.getStatusMeta(item).text === '通过').length
|
||||||
|
},
|
||||||
|
pendingCount() {
|
||||||
|
return this.records.filter((item) => this.getStatusMeta(item).text !== '通过').length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
this.loadData()
|
||||||
|
},
|
||||||
|
onPullDownRefresh() {
|
||||||
|
this.loadData(true)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getStatusMeta(item) {
|
||||||
|
return getReviewStatusMeta(item && item.reviewList)
|
||||||
|
},
|
||||||
|
async loadData(fromPullDown = false) {
|
||||||
|
this.loading = true
|
||||||
|
this.errorText = ''
|
||||||
|
try {
|
||||||
|
const result = await userPageQmgcForm({
|
||||||
|
page: 1,
|
||||||
|
limit: 20
|
||||||
|
})
|
||||||
|
this.records = (result && result.list) || []
|
||||||
|
} catch (error) {
|
||||||
|
this.errorText = (error && error.message) || '青马数据加载失败'
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
if (fromPullDown) {
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
copyValue(value) {
|
||||||
|
if (!value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '暂无可复制内容',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: String(value),
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '已复制',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goApplyList() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/gxmu/index'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top center, rgba(184, 146, 33, 0.1), transparent 30%),
|
||||||
|
linear-gradient(180deg, #f8f4eb 0%, #f4efe5 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-scroll {
|
||||||
|
height: 100vh;
|
||||||
|
padding: 24rpx 24rpx 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card,
|
||||||
|
.state-card,
|
||||||
|
.data-card {
|
||||||
|
border-radius: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
padding: 34rpx 36rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
box-shadow: 0 4rpx 12rpx 0 rgba(174, 174, 174, 0.4);
|
||||||
|
backdrop-filter: blur(24rpx);
|
||||||
|
-webkit-backdrop-filter: blur(24rpx);
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
padding-left: 10rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: #18a0f7;
|
||||||
|
border-left: 5px solid #0f94ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-desc {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #7f7f7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card,
|
||||||
|
.data-card {
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
border: 1rpx solid rgba(184, 146, 33, 0.08);
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(76, 49, 35, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
padding: 18rpx 16rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: rgba(255, 253, 244, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding: 28rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card--error {
|
||||||
|
border-color: rgba(195, 49, 30, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #20252c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-desc {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.75;
|
||||||
|
color: #786e69;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-action {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #a37a11;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-card {
|
||||||
|
padding: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #22272f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #8b817c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
padding: 10rpx 16rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--success {
|
||||||
|
background: #eef9f1;
|
||||||
|
color: #1f8f49;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--danger {
|
||||||
|
background: #fff1f0;
|
||||||
|
color: #d4380d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--warning {
|
||||||
|
background: #fff8e6;
|
||||||
|
color: #ad7b00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--default {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #8b817c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
padding: 18rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: #faf7f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-label {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #8b817c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-value {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #2d333b;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-action {
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(163, 122, 17, 0.08);
|
||||||
|
color: #a37a11;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
237
pages.json
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
{
|
||||||
|
"easycom": {
|
||||||
|
"autoscan": true,
|
||||||
|
"custom": {
|
||||||
|
"^zero-markdown-view$": "@/uni_modules/zero-markdown-view/components/zero-markdown-view/zero-markdown-view.vue"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "pages/index/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "首页",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/assistant/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "助手",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mine/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subPackages": [
|
||||||
|
{
|
||||||
|
"root": "pages/gxmu",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "五四评优",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "form",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "申报表",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tzbcy",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "挑战杯申报",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "qmgc",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "青马工程申报",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "wxxzx",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "未来学术之星申报",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "stats",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "数据统计与报表中心",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "cross-school-board",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "跨校活动情报看板",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tzb-database",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "创新竞赛历史项目数据库",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tzb-project-list",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "挑战杯项目库",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tzb-talent-list",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "挑战杯人才库",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "review-list",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "审核列表",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"root": "packageMine",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "profile",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "用户资料",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "qingma",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的青马",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "manuscripts",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的稿件",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "declare",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的申报",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"root": "pages/tygl",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "团员档案管理",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"root": "pages/manuscript",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "文稿工作台"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"root": "pages/activity-outline",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "活动策划大纲"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"root": "pages/creative-assistant",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "AI创意助手"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"root": "pages/login",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "登录"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"globalStyle": {
|
||||||
|
"navigationBarTextStyle": "white",
|
||||||
|
"navigationBarTitleText": "智慧团委",
|
||||||
|
"navigationBarBackgroundColor": "#1496F2",
|
||||||
|
"backgroundColor": "#F4FAFF"
|
||||||
|
},
|
||||||
|
"tabBar": {
|
||||||
|
"color": "#7D7672",
|
||||||
|
"selectedColor": "#1496F2",
|
||||||
|
"backgroundColor": "#FFFFFF",
|
||||||
|
"borderStyle": "black",
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"pagePath": "pages/index/index",
|
||||||
|
"iconPath": "static/tabbar/home.png",
|
||||||
|
"selectedIconPath": "static/tabbar/home-active.png",
|
||||||
|
"text": "首页"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/assistant/index",
|
||||||
|
"iconPath": "static/tabbar/chat.png",
|
||||||
|
"selectedIconPath": "static/tabbar/chat-active.png",
|
||||||
|
"text": "助手"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/mine/index",
|
||||||
|
"iconPath": "static/tabbar/user.png",
|
||||||
|
"selectedIconPath": "static/tabbar/user-active.png",
|
||||||
|
"text": "我的"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"uniIdRouter": {}
|
||||||
|
}
|
||||||
881
pages/activity-outline/index.vue
Normal file
@@ -0,0 +1,881 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<scroll-view scroll-y class="page-scroll">
|
||||||
|
<view class="hero-card">
|
||||||
|
<view class="hero-kicker">AI EVENT PLANNING OUTLINE</view>
|
||||||
|
<view class="hero-title">活动策划大纲生成</view>
|
||||||
|
<view class="hero-desc">
|
||||||
|
输入活动主题、受众、时间和资金等信息,系统会调用 `/ai/activityOutlineGen`
|
||||||
|
生成结构化活动策划大纲,适合方案初稿、汇报材料和执行拆解场景。
|
||||||
|
</view>
|
||||||
|
<view class="hero-actions">
|
||||||
|
<view class="hero-action" @tap="fillExample('volunteer')">志愿服务</view>
|
||||||
|
<view class="hero-action" @tap="fillExample('salon')">主题沙龙</view>
|
||||||
|
<view class="hero-action" @tap="fillExample('sports')">校园赛事</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="panel-card">
|
||||||
|
<view class="panel-title">活动信息</view>
|
||||||
|
<view class="field-list">
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">活动主题</view>
|
||||||
|
<uv-input
|
||||||
|
v-model="form.event_theme"
|
||||||
|
class="field-input"
|
||||||
|
placeholder="例如:青春志愿行 绿美校园环保行动"
|
||||||
|
placeholder-style="color: #c0c4cc;"
|
||||||
|
:maxlength="-1"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">面向受众</view>
|
||||||
|
<uv-input
|
||||||
|
v-model="form.people"
|
||||||
|
class="field-input"
|
||||||
|
placeholder="例如:全校团员青年、学生骨干、青年志愿者"
|
||||||
|
placeholder-style="color: #c0c4cc;"
|
||||||
|
:maxlength="-1"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">活动时间</view>
|
||||||
|
<uv-input
|
||||||
|
v-model="form.event_time"
|
||||||
|
class="field-input"
|
||||||
|
placeholder="例如:2026年4月18日 14:30-17:30"
|
||||||
|
placeholder-style="color: #c0c4cc;"
|
||||||
|
:maxlength="-1"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">活动资金</view>
|
||||||
|
<uv-input
|
||||||
|
v-model="form.event_funding"
|
||||||
|
class="field-input"
|
||||||
|
placeholder="例如:预算 5000 元,来源为校团委专项经费"
|
||||||
|
placeholder-style="color: #c0c4cc;"
|
||||||
|
:maxlength="-1"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">活动形式</view>
|
||||||
|
<uv-input
|
||||||
|
v-model="form.event_type"
|
||||||
|
class="field-input"
|
||||||
|
placeholder="例如:专题讲座 + 分组互动 + 现场展示"
|
||||||
|
placeholder-style="color: #c0c4cc;"
|
||||||
|
:maxlength="-1"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">活动地点</view>
|
||||||
|
<uv-input
|
||||||
|
v-model="form.event_location"
|
||||||
|
class="field-input"
|
||||||
|
placeholder="例如:大学生活动中心一楼报告厅"
|
||||||
|
placeholder-style="color: #c0c4cc;"
|
||||||
|
:maxlength="-1"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">备注</view>
|
||||||
|
<textarea
|
||||||
|
v-model="form.event_meme"
|
||||||
|
class="field-textarea"
|
||||||
|
placeholder="补充活动背景、目标、关键限制、预期成果或希望体现的亮点"
|
||||||
|
placeholder-style="color: #c0c4cc;"
|
||||||
|
maxlength="-1"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="submit-bar">
|
||||||
|
<view class="ghost-btn" @tap="resetForm">重置</view>
|
||||||
|
<view class="primary-btn" :class="{ 'primary-btn--disabled': !canSubmit || loading }" @tap="handleGenerate">
|
||||||
|
{{ loading ? '生成中' : '生成大纲' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="hint-block">
|
||||||
|
<view class="hint-title">提交说明</view>
|
||||||
|
<view class="hint-text">
|
||||||
|
必填字段会直接作为 `inputs` 透传给 AI,后端固定触发“活动策划大纲生成”指令,并返回整理后的正文结果。
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="panel-card result-card">
|
||||||
|
<view class="result-head">
|
||||||
|
<view class="panel-title">生成结果</view>
|
||||||
|
<view class="result-status" :class="loading ? 'result-status--loading' : hasOutlineContent ? 'result-status--done' : ''">
|
||||||
|
{{ loading ? '生成中' : hasOutlineContent ? '已生成' : '待生成' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading" class="loading-panel">
|
||||||
|
<view class="loading-kicker">OUTLINE GENERATING</view>
|
||||||
|
<view class="loading-title">AI 正在整理活动策划结构</view>
|
||||||
|
<view class="loading-desc">
|
||||||
|
正在综合活动主题、受众、时间与经费信息,生成可直接二次编辑的策划大纲。
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="showThinkingIndicator" class="outline-thinking">
|
||||||
|
<view class="thinking-dot"></view>
|
||||||
|
<text>AI 正在生成正文内容,请稍候...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="resultSummary" class="result-alert">
|
||||||
|
{{ resultSummary }}
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="hasOutlineContent || loading" class="field-item">
|
||||||
|
<view class="field-label">策划大纲</view>
|
||||||
|
<textarea
|
||||||
|
v-model="editableOutlineMarkdown"
|
||||||
|
class="result-textarea"
|
||||||
|
placeholder="填写左侧信息后开始生成活动策划大纲"
|
||||||
|
placeholder-style="color: #c0c4cc;"
|
||||||
|
maxlength="-1"
|
||||||
|
:disabled="loading"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else class="result-empty">
|
||||||
|
填写左侧信息后开始生成活动策划大纲
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="result-actions">
|
||||||
|
<view class="result-btn" @tap="copyOutline">复制内容</view>
|
||||||
|
<view class="result-btn" @tap="clearResult">清空结果</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
activityOutlineGen,
|
||||||
|
buildWebSocketUrl,
|
||||||
|
getCurrentUser
|
||||||
|
} from './service'
|
||||||
|
|
||||||
|
const STREAM_END_TEXT = '__END__'
|
||||||
|
const SOCKET_CONNECTED_TEXT = '连接成功'
|
||||||
|
const THINK_OPEN_TAG = '<think>'
|
||||||
|
const THINK_CLOSE_TAG = '</think>'
|
||||||
|
|
||||||
|
const createDefaultForm = () => ({
|
||||||
|
event_theme: '',
|
||||||
|
people: '',
|
||||||
|
event_time: '',
|
||||||
|
event_funding: '',
|
||||||
|
event_type: '',
|
||||||
|
event_location: '',
|
||||||
|
event_meme: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
form: createDefaultForm(),
|
||||||
|
loading: false,
|
||||||
|
result: null,
|
||||||
|
outlineMarkdown: '',
|
||||||
|
editableOutlineMarkdown: '',
|
||||||
|
resultSummary: '',
|
||||||
|
activeConversationId: '',
|
||||||
|
activeMessageId: '',
|
||||||
|
socketTask: null,
|
||||||
|
socketConnectPromise: null,
|
||||||
|
streamStarted: false,
|
||||||
|
streamResolver: null,
|
||||||
|
currentUser: {
|
||||||
|
userId: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
canSubmit() {
|
||||||
|
return !!(
|
||||||
|
String(this.form.event_theme || '').trim() &&
|
||||||
|
String(this.form.people || '').trim() &&
|
||||||
|
String(this.form.event_time || '').trim() &&
|
||||||
|
String(this.form.event_funding || '').trim()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
parsedOutlineContent() {
|
||||||
|
return this.parseAssistantContent(this.outlineMarkdown)
|
||||||
|
},
|
||||||
|
visibleOutlineMarkdown() {
|
||||||
|
return this.parsedOutlineContent.content
|
||||||
|
},
|
||||||
|
hasOutlineContent() {
|
||||||
|
return !!String(this.editableOutlineMarkdown || '').trim()
|
||||||
|
},
|
||||||
|
showThinkingIndicator() {
|
||||||
|
return this.loading && this.parsedOutlineContent.thinking
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onUnload() {
|
||||||
|
this.clearStreamResolver()
|
||||||
|
this.disconnectWebSocket()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showToast(title) {
|
||||||
|
uni.showToast({
|
||||||
|
title,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
parseAssistantContent(value) {
|
||||||
|
if (!value) {
|
||||||
|
return {
|
||||||
|
content: '',
|
||||||
|
thinking: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cursor = 0
|
||||||
|
let content = ''
|
||||||
|
let thinking = false
|
||||||
|
|
||||||
|
while (cursor < value.length) {
|
||||||
|
if (!thinking) {
|
||||||
|
const openIndex = value.indexOf(THINK_OPEN_TAG, cursor)
|
||||||
|
const closeIndex = value.indexOf(THINK_CLOSE_TAG, cursor)
|
||||||
|
|
||||||
|
if (closeIndex !== -1 && (openIndex === -1 || closeIndex < openIndex)) {
|
||||||
|
cursor = closeIndex + THINK_CLOSE_TAG.length
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (openIndex === -1) {
|
||||||
|
content += value.slice(cursor)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
content += value.slice(cursor, openIndex)
|
||||||
|
cursor = openIndex + THINK_OPEN_TAG.length
|
||||||
|
thinking = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeIndex = value.indexOf(THINK_CLOSE_TAG, cursor)
|
||||||
|
if (closeIndex === -1) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cursor = closeIndex + THINK_CLOSE_TAG.length
|
||||||
|
thinking = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
thinking
|
||||||
|
}
|
||||||
|
},
|
||||||
|
syncEditableOutline() {
|
||||||
|
this.editableOutlineMarkdown = this.visibleOutlineMarkdown
|
||||||
|
},
|
||||||
|
buildPayload() {
|
||||||
|
return {
|
||||||
|
event_theme: String(this.form.event_theme || '').trim(),
|
||||||
|
people: String(this.form.people || '').trim(),
|
||||||
|
event_time: String(this.form.event_time || '').trim(),
|
||||||
|
event_funding: String(this.form.event_funding || '').trim(),
|
||||||
|
event_type: String(this.form.event_type || '').trim(),
|
||||||
|
event_location: String(this.form.event_location || '').trim(),
|
||||||
|
event_meme: String(this.form.event_meme || '').trim()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pickNonEmptyString() {
|
||||||
|
for (let index = 0; index < arguments.length; index += 1) {
|
||||||
|
const value = arguments[index]
|
||||||
|
if (typeof value === 'string' && value.length > 0) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
pickRawString() {
|
||||||
|
for (let index = 0; index < arguments.length; index += 1) {
|
||||||
|
const value = arguments[index]
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
},
|
||||||
|
stringifyUnknown(value) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value, null, 2)
|
||||||
|
} catch (error) {
|
||||||
|
return String(value || '')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseOutline(payload) {
|
||||||
|
if (!payload) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const answer = this.pickNonEmptyString(
|
||||||
|
payload.answer,
|
||||||
|
payload.raw && payload.raw.answer,
|
||||||
|
payload.raw && payload.raw.data && payload.raw.data.answer,
|
||||||
|
payload.raw && payload.raw.output,
|
||||||
|
payload.raw && payload.raw.result,
|
||||||
|
payload.raw && payload.raw.content,
|
||||||
|
payload.raw && payload.raw.message
|
||||||
|
)
|
||||||
|
if (answer) {
|
||||||
|
return answer
|
||||||
|
}
|
||||||
|
return this.stringifyUnknown((payload && payload.raw) || payload)
|
||||||
|
},
|
||||||
|
buildSummary() {
|
||||||
|
const parts = []
|
||||||
|
if (this.activeConversationId) {
|
||||||
|
parts.push(`conversationId: ${this.activeConversationId}`)
|
||||||
|
}
|
||||||
|
if (this.activeMessageId) {
|
||||||
|
parts.push(`messageId: ${this.activeMessageId}`)
|
||||||
|
}
|
||||||
|
return parts.join(' | ')
|
||||||
|
},
|
||||||
|
clearStreamResolver() {
|
||||||
|
this.streamResolver = null
|
||||||
|
},
|
||||||
|
resolveStream() {
|
||||||
|
if (this.streamResolver && this.streamResolver.resolve) {
|
||||||
|
this.streamResolver.resolve()
|
||||||
|
}
|
||||||
|
this.clearStreamResolver()
|
||||||
|
},
|
||||||
|
rejectStream(error) {
|
||||||
|
if (this.streamResolver && this.streamResolver.reject) {
|
||||||
|
this.streamResolver.reject(error)
|
||||||
|
}
|
||||||
|
this.clearStreamResolver()
|
||||||
|
},
|
||||||
|
createStreamWaiter() {
|
||||||
|
this.streamStarted = false
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.streamResolver = {
|
||||||
|
resolve,
|
||||||
|
reject
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
disconnectWebSocket() {
|
||||||
|
this.socketConnectPromise = null
|
||||||
|
const socketTask = this.socketTask
|
||||||
|
this.socketTask = null
|
||||||
|
if (socketTask && typeof socketTask.close === 'function') {
|
||||||
|
try {
|
||||||
|
socketTask.close({})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('socket close failed', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
appendOutlineChunk(chunk, conversationId, messageId) {
|
||||||
|
if (!chunk) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (conversationId && conversationId !== '0') {
|
||||||
|
this.activeConversationId = conversationId
|
||||||
|
}
|
||||||
|
if (messageId && messageId !== '0') {
|
||||||
|
this.activeMessageId = messageId
|
||||||
|
}
|
||||||
|
if (!this.streamStarted) {
|
||||||
|
this.streamStarted = true
|
||||||
|
this.outlineMarkdown = ''
|
||||||
|
}
|
||||||
|
this.outlineMarkdown += chunk
|
||||||
|
this.syncEditableOutline()
|
||||||
|
this.resultSummary = this.buildSummary() || '正在通过 WebSocket 接收流式内容'
|
||||||
|
},
|
||||||
|
finishStream(conversationId, messageId) {
|
||||||
|
if (conversationId && conversationId !== '0') {
|
||||||
|
this.activeConversationId = conversationId
|
||||||
|
}
|
||||||
|
if (messageId && messageId !== '0') {
|
||||||
|
this.activeMessageId = messageId
|
||||||
|
}
|
||||||
|
this.resultSummary = this.buildSummary() || '已通过 WebSocket 接收生成结果'
|
||||||
|
this.loading = false
|
||||||
|
this.resolveStream()
|
||||||
|
this.disconnectWebSocket()
|
||||||
|
},
|
||||||
|
handleSocketMessage(raw) {
|
||||||
|
if (!raw) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(raw)
|
||||||
|
if (data && data.answer === STREAM_END_TEXT) {
|
||||||
|
this.finishStream(data.conversationId, data.messageId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (data && (data.loading === 'true' || data.loading === true)) {
|
||||||
|
this.resultSummary = 'AI 正在组织活动策划大纲结构...'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const chunk = this.pickRawString(data && data.notice, data && data.answer)
|
||||||
|
if (typeof chunk === 'string') {
|
||||||
|
this.appendOutlineChunk(chunk, data && data.conversationId, data && data.messageId)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.appendOutlineChunk(raw)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refreshUser() {
|
||||||
|
this.currentUser = getCurrentUser()
|
||||||
|
return this.currentUser
|
||||||
|
},
|
||||||
|
connectWebSocket() {
|
||||||
|
const user = this.refreshUser()
|
||||||
|
if (!user.userId) {
|
||||||
|
return Promise.reject(new Error('未获取到当前登录用户'))
|
||||||
|
}
|
||||||
|
const currentSocket = this.socketTask
|
||||||
|
if (currentSocket && this.socketConnectPromise) {
|
||||||
|
return this.socketConnectPromise
|
||||||
|
}
|
||||||
|
if (currentSocket) {
|
||||||
|
this.disconnectWebSocket()
|
||||||
|
}
|
||||||
|
|
||||||
|
let cleanupPending = () => undefined
|
||||||
|
const connectPromise = new Promise((resolve, reject) => {
|
||||||
|
let settled = false
|
||||||
|
const socketTask = uni.connectSocket({
|
||||||
|
url: buildWebSocketUrl(user.userId),
|
||||||
|
complete: () => {}
|
||||||
|
})
|
||||||
|
|
||||||
|
cleanupPending = () => {
|
||||||
|
if (this.socketConnectPromise === connectPromise) {
|
||||||
|
this.socketConnectPromise = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
socketTask.onMessage((event) => {
|
||||||
|
const raw = String((event && event.data) || '')
|
||||||
|
if (raw === SOCKET_CONNECTED_TEXT) {
|
||||||
|
if (!settled) {
|
||||||
|
settled = true
|
||||||
|
cleanupPending()
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.handleSocketMessage(raw)
|
||||||
|
})
|
||||||
|
socketTask.onClose(() => {
|
||||||
|
if (this.socketTask === socketTask) {
|
||||||
|
this.socketTask = null
|
||||||
|
}
|
||||||
|
if (!settled) {
|
||||||
|
settled = true
|
||||||
|
cleanupPending()
|
||||||
|
reject(new Error('活动策划大纲连接已关闭'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.loading) {
|
||||||
|
this.rejectStream(new Error('活动策划大纲连接已关闭'))
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
cleanupPending()
|
||||||
|
})
|
||||||
|
socketTask.onError(() => {
|
||||||
|
if (!settled) {
|
||||||
|
settled = true
|
||||||
|
cleanupPending()
|
||||||
|
reject(new Error('活动策划大纲连接失败'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.loading) {
|
||||||
|
this.rejectStream(new Error('活动策划大纲连接失败'))
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.socketTask = socketTask
|
||||||
|
})
|
||||||
|
this.socketConnectPromise = connectPromise
|
||||||
|
return connectPromise
|
||||||
|
},
|
||||||
|
clearResult() {
|
||||||
|
this.result = null
|
||||||
|
this.outlineMarkdown = ''
|
||||||
|
this.editableOutlineMarkdown = ''
|
||||||
|
this.resultSummary = ''
|
||||||
|
this.activeConversationId = ''
|
||||||
|
this.activeMessageId = ''
|
||||||
|
},
|
||||||
|
resetForm() {
|
||||||
|
this.form = createDefaultForm()
|
||||||
|
this.clearResult()
|
||||||
|
},
|
||||||
|
async handleGenerate() {
|
||||||
|
if (this.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.canSubmit) {
|
||||||
|
this.showToast('请先填写全部必填项')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.loading = true
|
||||||
|
this.clearResult()
|
||||||
|
this.resultSummary = '正在建立 WebSocket 连接...'
|
||||||
|
try {
|
||||||
|
await this.connectWebSocket()
|
||||||
|
this.resultSummary = '连接成功,已提交生成任务,等待流式返回...'
|
||||||
|
const waitStream = this.createStreamWaiter()
|
||||||
|
const data = await activityOutlineGen(this.buildPayload())
|
||||||
|
this.result = data || null
|
||||||
|
if (!this.streamStarted && data && this.parseOutline(data)) {
|
||||||
|
this.outlineMarkdown = this.parseOutline(data)
|
||||||
|
this.syncEditableOutline()
|
||||||
|
this.resultSummary = this.buildSummary() || '接口直接返回生成结果'
|
||||||
|
this.loading = false
|
||||||
|
this.resolveStream()
|
||||||
|
this.disconnectWebSocket()
|
||||||
|
} else {
|
||||||
|
await waitStream
|
||||||
|
}
|
||||||
|
if (!this.visibleOutlineMarkdown) {
|
||||||
|
this.showToast('接口已返回,但未解析到正文内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.showToast('活动策划大纲生成完成')
|
||||||
|
} catch (error) {
|
||||||
|
this.disconnectWebSocket()
|
||||||
|
this.clearStreamResolver()
|
||||||
|
this.loading = false
|
||||||
|
this.showToast((error && error.message) || '生成失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
copyOutline() {
|
||||||
|
if (!this.editableOutlineMarkdown) {
|
||||||
|
this.showToast('暂无可复制内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: this.editableOutlineMarkdown,
|
||||||
|
success: () => {
|
||||||
|
this.showToast('内容已复制')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fillExample(type) {
|
||||||
|
if (type === 'volunteer') {
|
||||||
|
this.form.event_theme = '青春志愿行 绿美校园环保行动'
|
||||||
|
this.form.people = '全校团员青年、学生骨干、青年志愿者'
|
||||||
|
this.form.event_time = '2026年4月18日 14:30-17:30'
|
||||||
|
this.form.event_funding = '预算 5000 元,来源为校团委专项经费'
|
||||||
|
this.form.event_type = '志愿服务 + 分组清洁 + 环保倡议'
|
||||||
|
this.form.event_location = '校园主干道、教学楼周边和青年林'
|
||||||
|
this.form.event_meme = '突出劳动教育、志愿服务精神和绿色校园建设,生成含活动背景、流程、分工、宣传和风险预案的大纲。'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (type === 'salon') {
|
||||||
|
this.form.event_theme = '青春思享汇 新时代青年成长主题沙龙'
|
||||||
|
this.form.people = '学生干部、团支部书记、青年学生代表'
|
||||||
|
this.form.event_time = '2026年5月10日 19:00-21:00'
|
||||||
|
this.form.event_funding = '预算 3000 元,包含物料、海报和场地布置'
|
||||||
|
this.form.event_type = '主题分享 + 圆桌讨论 + 互动提问'
|
||||||
|
this.form.event_location = '大学生活动中心多功能厅'
|
||||||
|
this.form.event_meme = '希望突出思想引领、青年交流和成果展示,生成适合汇报审批的结构化活动方案。'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.form.event_theme = '青春杯校园三人篮球赛'
|
||||||
|
this.form.people = '各学院学生代表队、体育类社团、观赛学生'
|
||||||
|
this.form.event_time = '2026年4月下旬周末全天'
|
||||||
|
this.form.event_funding = '预算 8000 元,含裁判、奖品、宣传和后勤保障'
|
||||||
|
this.form.event_type = '校园赛事 + 开幕仪式 + 分组淘汰赛'
|
||||||
|
this.form.event_location = '学校室外篮球场'
|
||||||
|
this.form.event_meme = '需要体现赛事组织、赛程安排、安全保障、医疗预案和宣传报道计划。'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top right, rgba(51, 97, 164, 0.14), transparent 26%),
|
||||||
|
linear-gradient(180deg, #f7f9fc 0%, #f2f4f8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-scroll {
|
||||||
|
height: 100vh;
|
||||||
|
padding: 28rpx 24rpx 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
padding: 34rpx 30rpx;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
background: linear-gradient(135deg, #16315f 0%, #26508e 52%, #4b79bb 100%);
|
||||||
|
box-shadow: 0 18rpx 44rpx rgba(33, 67, 121, 0.18);
|
||||||
|
color: #eef5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-kicker {
|
||||||
|
font-size: 22rpx;
|
||||||
|
letter-spacing: 4rpx;
|
||||||
|
color: rgba(238, 245, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
font-size: 42rpx;
|
||||||
|
line-height: 1.35;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-desc {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: rgba(238, 245, 255, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-top: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-action {
|
||||||
|
padding: 12rpx 20rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #f4f8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-card {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding: 26rpx;
|
||||||
|
border-radius: 30rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
border: 1rpx solid rgba(51, 97, 164, 0.08);
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(61, 73, 95, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 22rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
font-size: 25rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2b3037;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 220rpx;
|
||||||
|
padding: 22rpx 24rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: #f6f8fb;
|
||||||
|
border: 1rpx solid #dde4ef;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #2a2e35;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.field-input.uv-input) {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 88rpx;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
background: #ffffff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.field-input.uv-input .uv-input__content__field-wrapper__field) {
|
||||||
|
height: 88rpx;
|
||||||
|
min-height: 88rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 88rpx;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-bar,
|
||||||
|
.result-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 18rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost-btn,
|
||||||
|
.primary-btn,
|
||||||
|
.result-btn {
|
||||||
|
height: 88rpx;
|
||||||
|
line-height: 88rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost-btn {
|
||||||
|
background: #f6f7fa;
|
||||||
|
color: #50617a;
|
||||||
|
border: 1rpx solid #d8dfeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
background: linear-gradient(135deg, #1f4f8f 0%, #3f70b5 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn--disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-btn {
|
||||||
|
background: #eef4fb;
|
||||||
|
color: #24508f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-block,
|
||||||
|
.result-alert {
|
||||||
|
margin-top: 22rpx;
|
||||||
|
padding: 22rpx 24rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #f5f8fd;
|
||||||
|
border: 1rpx solid rgba(51, 97, 164, 0.08);
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.75;
|
||||||
|
color: #6d7d93;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-title {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #35527d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-text {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-status {
|
||||||
|
padding: 10rpx 16rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #eef1f5;
|
||||||
|
color: #7e8792;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-status--loading {
|
||||||
|
background: #edf5ff;
|
||||||
|
color: #1554ad;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-status--done {
|
||||||
|
background: #eef9f1;
|
||||||
|
color: #1f8f49;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-panel {
|
||||||
|
margin-top: 22rpx;
|
||||||
|
padding: 26rpx;
|
||||||
|
border-radius: 26rpx;
|
||||||
|
background: linear-gradient(135deg, #f5f9ff 0%, #eef4fb 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-kicker {
|
||||||
|
font-size: 20rpx;
|
||||||
|
letter-spacing: 3rpx;
|
||||||
|
color: #6682aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-title {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #23446f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-desc {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.75;
|
||||||
|
color: #6d7d93;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outline-thinking {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #35527d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-dot {
|
||||||
|
width: 16rpx;
|
||||||
|
height: 16rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #4b79bb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 560rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1rpx solid #e1e7f0;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.85;
|
||||||
|
color: #2b3037;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-empty {
|
||||||
|
margin-top: 22rpx;
|
||||||
|
padding: 32rpx 24rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #f8fafc;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.75;
|
||||||
|
color: #8a94a0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
17
pages/activity-outline/service.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import {
|
||||||
|
API_BASE_URL,
|
||||||
|
apiRequest,
|
||||||
|
buildWebSocketUrl,
|
||||||
|
getCurrentUser
|
||||||
|
} from '../assistant/chat-service'
|
||||||
|
|
||||||
|
export { buildWebSocketUrl, getCurrentUser }
|
||||||
|
|
||||||
|
export function activityOutlineGen(data) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${API_BASE_URL}/ai/activityOutlineGen`,
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
withTenant: true
|
||||||
|
})
|
||||||
|
}
|
||||||
447
pages/assistant/chat-service.js
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
import {
|
||||||
|
API_BASE_URL,
|
||||||
|
DEFAULT_TENANT_ID,
|
||||||
|
TOKEN_HEADER_NAME,
|
||||||
|
clearAuthStorage,
|
||||||
|
getStoredUserInfo,
|
||||||
|
getToken,
|
||||||
|
redirectToLogin
|
||||||
|
} from '../../utils/request'
|
||||||
|
|
||||||
|
export { API_BASE_URL, getToken }
|
||||||
|
|
||||||
|
const MODULES_API_URL = API_BASE_URL
|
||||||
|
const APP_SECRET = 'ffd6eee985af45e4a75098422d1decbb'
|
||||||
|
const AI_REQUEST_TIMEOUT = 10 * 60 * 1000
|
||||||
|
|
||||||
|
let realAtob
|
||||||
|
const b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
|
||||||
|
const b64re = /^(?:[A-Za-z\d+/]{4})*?(?:[A-Za-z\d+/]{2}(?:==)?|[A-Za-z\d+/]{3}=?)?$/
|
||||||
|
|
||||||
|
if (typeof atob !== 'function') {
|
||||||
|
realAtob = function(str) {
|
||||||
|
let input = String(str || '').replace(/[\t\n\f\r ]+/g, '')
|
||||||
|
if (!b64re.test(input)) {
|
||||||
|
throw new Error("Failed to execute 'atob': The string to be decoded is not correctly encoded.")
|
||||||
|
}
|
||||||
|
input += '=='.slice(2 - (input.length & 3))
|
||||||
|
let bitmap
|
||||||
|
let result = ''
|
||||||
|
let r1
|
||||||
|
let r2
|
||||||
|
let index = 0
|
||||||
|
for (; index < input.length;) {
|
||||||
|
bitmap =
|
||||||
|
(b64.indexOf(input.charAt(index++)) << 18) |
|
||||||
|
(b64.indexOf(input.charAt(index++)) << 12) |
|
||||||
|
((r1 = b64.indexOf(input.charAt(index++))) << 6) |
|
||||||
|
(r2 = b64.indexOf(input.charAt(index++)))
|
||||||
|
result +=
|
||||||
|
r1 === 64
|
||||||
|
? String.fromCharCode((bitmap >> 16) & 255)
|
||||||
|
: r2 === 64
|
||||||
|
? String.fromCharCode((bitmap >> 16) & 255, (bitmap >> 8) & 255)
|
||||||
|
: String.fromCharCode((bitmap >> 16) & 255, (bitmap >> 8) & 255, bitmap & 255)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
realAtob = atob
|
||||||
|
}
|
||||||
|
|
||||||
|
function b64DecodeUnicode(str) {
|
||||||
|
return decodeURIComponent(
|
||||||
|
realAtob(str)
|
||||||
|
.split('')
|
||||||
|
.map((char) => `%${`00${char.charCodeAt(0).toString(16)}`.slice(-2)}`)
|
||||||
|
.join('')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTokenPayload(token) {
|
||||||
|
const rawToken = String(token || '').replace(/^Bearer\s+/i, '')
|
||||||
|
const parts = rawToken.split('.')
|
||||||
|
if (!rawToken || parts.length !== 3) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const normalized = parts[1].replace(/-/g, '+').replace(/_/g, '/')
|
||||||
|
return JSON.parse(b64DecodeUnicode(normalized))
|
||||||
|
} catch (error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeAdd(x, y) {
|
||||||
|
const lsw = (x & 0xffff) + (y & 0xffff)
|
||||||
|
const msw = (x >> 16) + (y >> 16) + (lsw >> 16)
|
||||||
|
return (msw << 16) | (lsw & 0xffff)
|
||||||
|
}
|
||||||
|
|
||||||
|
function bitRotateLeft(num, cnt) {
|
||||||
|
return (num << cnt) | (num >>> (32 - cnt))
|
||||||
|
}
|
||||||
|
|
||||||
|
function md5cmn(q, a, b, x, s, t) {
|
||||||
|
return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b)
|
||||||
|
}
|
||||||
|
|
||||||
|
function md5ff(a, b, c, d, x, s, t) {
|
||||||
|
return md5cmn((b & c) | (~b & d), a, b, x, s, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
function md5gg(a, b, c, d, x, s, t) {
|
||||||
|
return md5cmn((b & d) | (c & ~d), a, b, x, s, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
function md5hh(a, b, c, d, x, s, t) {
|
||||||
|
return md5cmn(b ^ c ^ d, a, b, x, s, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
function md5ii(a, b, c, d, x, s, t) {
|
||||||
|
return md5cmn(c ^ (b | ~d), a, b, x, s, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
function binlMD5(x, len) {
|
||||||
|
x[len >> 5] |= 0x80 << len % 32
|
||||||
|
x[(((len + 64) >>> 9) << 4) + 14] = len
|
||||||
|
|
||||||
|
let i
|
||||||
|
let olda
|
||||||
|
let oldb
|
||||||
|
let oldc
|
||||||
|
let oldd
|
||||||
|
let a = 1732584193
|
||||||
|
let b = -271733879
|
||||||
|
let c = -1732584194
|
||||||
|
let d = 271733878
|
||||||
|
|
||||||
|
for (i = 0; i < x.length; i += 16) {
|
||||||
|
olda = a
|
||||||
|
oldb = b
|
||||||
|
oldc = c
|
||||||
|
oldd = d
|
||||||
|
|
||||||
|
a = md5ff(a, b, c, d, x[i], 7, -680876936)
|
||||||
|
d = md5ff(d, a, b, c, x[i + 1], 12, -389564586)
|
||||||
|
c = md5ff(c, d, a, b, x[i + 2], 17, 606105819)
|
||||||
|
b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330)
|
||||||
|
a = md5ff(a, b, c, d, x[i + 4], 7, -176418897)
|
||||||
|
d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426)
|
||||||
|
c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341)
|
||||||
|
b = md5ff(b, c, d, a, x[i + 7], 22, -45705983)
|
||||||
|
a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416)
|
||||||
|
d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417)
|
||||||
|
c = md5ff(c, d, a, b, x[i + 10], 17, -42063)
|
||||||
|
b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162)
|
||||||
|
a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682)
|
||||||
|
d = md5ff(d, a, b, c, x[i + 13], 12, -40341101)
|
||||||
|
c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290)
|
||||||
|
b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329)
|
||||||
|
|
||||||
|
a = md5gg(a, b, c, d, x[i + 1], 5, -165796510)
|
||||||
|
d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632)
|
||||||
|
c = md5gg(c, d, a, b, x[i + 11], 14, 643717713)
|
||||||
|
b = md5gg(b, c, d, a, x[i], 20, -373897302)
|
||||||
|
a = md5gg(a, b, c, d, x[i + 5], 5, -701558691)
|
||||||
|
d = md5gg(d, a, b, c, x[i + 10], 9, 38016083)
|
||||||
|
c = md5gg(c, d, a, b, x[i + 15], 14, -660478335)
|
||||||
|
b = md5gg(b, c, d, a, x[i + 4], 20, -405537848)
|
||||||
|
a = md5gg(a, b, c, d, x[i + 9], 5, 568446438)
|
||||||
|
d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690)
|
||||||
|
c = md5gg(c, d, a, b, x[i + 3], 14, -187363961)
|
||||||
|
b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501)
|
||||||
|
a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467)
|
||||||
|
d = md5gg(d, a, b, c, x[i + 2], 9, -51403784)
|
||||||
|
c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473)
|
||||||
|
b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734)
|
||||||
|
|
||||||
|
a = md5hh(a, b, c, d, x[i + 5], 4, -378558)
|
||||||
|
d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463)
|
||||||
|
c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562)
|
||||||
|
b = md5hh(b, c, d, a, x[i + 14], 23, -35309556)
|
||||||
|
a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060)
|
||||||
|
d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353)
|
||||||
|
c = md5hh(c, d, a, b, x[i + 7], 16, -155497632)
|
||||||
|
b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640)
|
||||||
|
a = md5hh(a, b, c, d, x[i + 13], 4, 681279174)
|
||||||
|
d = md5hh(d, a, b, c, x[i], 11, -358537222)
|
||||||
|
c = md5hh(c, d, a, b, x[i + 3], 16, -722521979)
|
||||||
|
b = md5hh(b, c, d, a, x[i + 6], 23, 76029189)
|
||||||
|
a = md5hh(a, b, c, d, x[i + 9], 4, -640364487)
|
||||||
|
d = md5hh(d, a, b, c, x[i + 12], 11, -421815835)
|
||||||
|
c = md5hh(c, d, a, b, x[i + 15], 16, 530742520)
|
||||||
|
b = md5hh(b, c, d, a, x[i + 2], 23, -995338651)
|
||||||
|
|
||||||
|
a = md5ii(a, b, c, d, x[i], 6, -198630844)
|
||||||
|
d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415)
|
||||||
|
c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905)
|
||||||
|
b = md5ii(b, c, d, a, x[i + 5], 21, -57434055)
|
||||||
|
a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571)
|
||||||
|
d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606)
|
||||||
|
c = md5ii(c, d, a, b, x[i + 10], 15, -1051523)
|
||||||
|
b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799)
|
||||||
|
a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359)
|
||||||
|
d = md5ii(d, a, b, c, x[i + 15], 10, -30611744)
|
||||||
|
c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380)
|
||||||
|
b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649)
|
||||||
|
a = md5ii(a, b, c, d, x[i + 4], 6, -145523070)
|
||||||
|
d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379)
|
||||||
|
c = md5ii(c, d, a, b, x[i + 2], 15, 718787259)
|
||||||
|
b = md5ii(b, c, d, a, x[i + 9], 21, -343485551)
|
||||||
|
|
||||||
|
a = safeAdd(a, olda)
|
||||||
|
b = safeAdd(b, oldb)
|
||||||
|
c = safeAdd(c, oldc)
|
||||||
|
d = safeAdd(d, oldd)
|
||||||
|
}
|
||||||
|
return [a, b, c, d]
|
||||||
|
}
|
||||||
|
|
||||||
|
function binl2rstr(input) {
|
||||||
|
let index
|
||||||
|
let output = ''
|
||||||
|
const length32 = input.length * 32
|
||||||
|
for (index = 0; index < length32; index += 8) {
|
||||||
|
output += String.fromCharCode((input[index >> 5] >>> index % 32) & 0xff)
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
function rstr2binl(input) {
|
||||||
|
const output = Array(input.length >> 2)
|
||||||
|
let index
|
||||||
|
for (index = 0; index < output.length; index += 1) {
|
||||||
|
output[index] = 0
|
||||||
|
}
|
||||||
|
const length8 = input.length * 8
|
||||||
|
for (index = 0; index < length8; index += 8) {
|
||||||
|
output[index >> 5] |= (input.charCodeAt(index / 8) & 0xff) << index % 32
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
function rstrMD5(value) {
|
||||||
|
return binl2rstr(binlMD5(rstr2binl(value), value.length * 8))
|
||||||
|
}
|
||||||
|
|
||||||
|
function rstr2hex(input) {
|
||||||
|
const hexTab = '0123456789abcdef'
|
||||||
|
let output = ''
|
||||||
|
let index
|
||||||
|
let value
|
||||||
|
for (index = 0; index < input.length; index += 1) {
|
||||||
|
value = input.charCodeAt(index)
|
||||||
|
output += hexTab.charAt((value >>> 4) & 0x0f) + hexTab.charAt(value & 0x0f)
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
function str2rstrUTF8(input) {
|
||||||
|
return unescape(encodeURIComponent(input))
|
||||||
|
}
|
||||||
|
|
||||||
|
function md5(value) {
|
||||||
|
return rstr2hex(rstrMD5(str2rstrUTF8(String(value || ''))))
|
||||||
|
}
|
||||||
|
|
||||||
|
function objKeySort(obj) {
|
||||||
|
const next = {}
|
||||||
|
Object.keys(obj || {})
|
||||||
|
.sort()
|
||||||
|
.forEach((key) => {
|
||||||
|
next[key] = obj[key]
|
||||||
|
})
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSignature(payload) {
|
||||||
|
const form = payload || {}
|
||||||
|
form.timestamp = Date.now()
|
||||||
|
form.version = 'v3'
|
||||||
|
let sign = ''
|
||||||
|
const sorted = objKeySort(form)
|
||||||
|
Object.keys(sorted).forEach((key) => {
|
||||||
|
const value = form[key]
|
||||||
|
if (value !== null && value !== undefined && value !== '') {
|
||||||
|
sign = `${sign}${value}-`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
form.sign = md5(`${sign}${APP_SECRET}`)
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStorageValue(keys) {
|
||||||
|
for (let index = 0; index < keys.length; index += 1) {
|
||||||
|
const value = uni.getStorageSync(keys[index])
|
||||||
|
if (value) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentUser() {
|
||||||
|
const primaryToken = getToken()
|
||||||
|
const storedUser = getStoredUserInfo()
|
||||||
|
const tokenList = [
|
||||||
|
primaryToken,
|
||||||
|
uni.getStorageSync('uni_id_token'),
|
||||||
|
uni.getStorageSync('access_token')
|
||||||
|
].filter(Boolean)
|
||||||
|
let payload = null
|
||||||
|
|
||||||
|
for (let index = 0; index < tokenList.length; index += 1) {
|
||||||
|
payload = parseTokenPayload(tokenList[index])
|
||||||
|
if (payload) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
return {
|
||||||
|
...storedUser,
|
||||||
|
token: primaryToken || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...storedUser,
|
||||||
|
userId: Number(payload.userId || payload.uid || payload.id || 0) || null,
|
||||||
|
nickname: payload.nickname || payload.username || payload.nickName || '',
|
||||||
|
realName: payload.realName || payload.realname || payload.name || '',
|
||||||
|
token: primaryToken || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function apiRequest({
|
||||||
|
url,
|
||||||
|
method = 'GET',
|
||||||
|
data,
|
||||||
|
withSignature = true,
|
||||||
|
withTenant = false,
|
||||||
|
timeout
|
||||||
|
}) {
|
||||||
|
const token = getToken()
|
||||||
|
const requestData = data ? { ...data } : undefined
|
||||||
|
const headers = {}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers[TOKEN_HEADER_NAME] = token
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestData && withTenant) {
|
||||||
|
requestData.tenantId = DEFAULT_TENANT_ID
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestData && withSignature) {
|
||||||
|
addSignature(requestData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return uni.request({
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
data: requestData,
|
||||||
|
header: headers,
|
||||||
|
...(timeout !== undefined && { timeout })
|
||||||
|
}).then((response) => {
|
||||||
|
const responseHeader = (response && response.header) || {}
|
||||||
|
const nextToken =
|
||||||
|
responseHeader[TOKEN_HEADER_NAME] ||
|
||||||
|
responseHeader[TOKEN_HEADER_NAME.toLowerCase()]
|
||||||
|
if (nextToken) {
|
||||||
|
uni.setStorageSync('access_token', nextToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { statusCode } = response || {}
|
||||||
|
const body = (response && response.data) || {}
|
||||||
|
if (statusCode && statusCode >= 400) {
|
||||||
|
return Promise.reject(new Error(body.message || '请求失败'))
|
||||||
|
}
|
||||||
|
if (Number(body.code) === 401) {
|
||||||
|
clearAuthStorage()
|
||||||
|
redirectToLogin()
|
||||||
|
return Promise.reject(new Error(body.message || '登录已失效'))
|
||||||
|
}
|
||||||
|
if (body.code === 0) {
|
||||||
|
return body.data !== undefined ? body.data : body.message
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(body.message || '请求失败'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listAiChatList(params) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${MODULES_API_URL}/ai/ai-chat-list`,
|
||||||
|
method: 'GET',
|
||||||
|
data: params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listAiChatHistory(params) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${MODULES_API_URL}/ai/ai-chat-history`,
|
||||||
|
method: 'GET',
|
||||||
|
data: params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeAiChatList(id) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${MODULES_API_URL}/ai/ai-chat-list/${id}`,
|
||||||
|
method: 'DELETE',
|
||||||
|
withSignature: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendMessage(data) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${API_BASE_URL}/ai/chat/message`,
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
withTenant: true,
|
||||||
|
timeout: AI_REQUEST_TIMEOUT
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function manuscriptGen(data) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${API_BASE_URL}/ai/manuscriptGen`,
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
withTenant: true,
|
||||||
|
timeout: AI_REQUEST_TIMEOUT
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function creativeAssistant(data) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${API_BASE_URL}/ai/creativeAssistant`,
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
withTenant: true,
|
||||||
|
timeout: AI_REQUEST_TIMEOUT
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function activityOutlineGen(data) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${API_BASE_URL}/ai/activityOutlineGen`,
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
withTenant: true,
|
||||||
|
timeout: AI_REQUEST_TIMEOUT
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildWebSocketUrl(userId) {
|
||||||
|
const url = String(API_BASE_URL || '')
|
||||||
|
if (/^https?:\/\//.test(url)) {
|
||||||
|
return `${url
|
||||||
|
.replace(/^https:/, 'wss:')
|
||||||
|
.replace(/^http:/, 'ws:')
|
||||||
|
.replace(/\/api\/?$/, '')
|
||||||
|
.replace(/\/$/, '')}/chat/${userId}`
|
||||||
|
}
|
||||||
|
return `${url.replace(/\/api\/?$/, '')}/chat/${userId}`
|
||||||
|
}
|
||||||
1725
pages/assistant/index.vue
Normal file
1038
pages/creative-assistant/index.vue
Normal file
45
pages/creative-assistant/service.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { API_BASE_URL, apiRequest, getToken } from '../assistant/chat-service'
|
||||||
|
|
||||||
|
export function creativeAssistant(data) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${API_BASE_URL}/ai/creativeAssistant`,
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
withTenant: true,
|
||||||
|
timeout: 600000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadAiFile(file) {
|
||||||
|
const token = getToken()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.uploadFile({
|
||||||
|
url: `${API_BASE_URL}/ai/file/upload`,
|
||||||
|
filePath: file.path,
|
||||||
|
name: 'file',
|
||||||
|
header: token
|
||||||
|
? {
|
||||||
|
Authorization: token
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
formData: {
|
||||||
|
tenantId: '10049'
|
||||||
|
},
|
||||||
|
success: (response) => {
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(response.data || '{}')
|
||||||
|
if (result.code === 0 && result.data) {
|
||||||
|
resolve(result.data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reject(new Error(result.message || '上传失败'))
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error('上传响应解析失败'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
reject(new Error(error.errMsg || '上传失败'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
395
pages/gxmu/config.js
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
const GENDER_OPTIONS = ['男', '女']
|
||||||
|
const YES_NO_OPTIONS = ['是', '否']
|
||||||
|
|
||||||
|
export const GXMU_MODULES = [
|
||||||
|
{
|
||||||
|
title: '十佳青年岗位能手',
|
||||||
|
code: 'gxmu_sjqn',
|
||||||
|
icon: '青',
|
||||||
|
group: '个人奖项',
|
||||||
|
desc: '面向青年岗位骨干与先进典型的申报入口。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '十佳团支部书记',
|
||||||
|
code: 'gxmu_sjtbzbsj',
|
||||||
|
icon: '书',
|
||||||
|
group: '个人奖项',
|
||||||
|
desc: '聚焦基层团支部书记的履职表现与带动成效。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '五四红旗团委',
|
||||||
|
code: 'gxmu_wshqtw',
|
||||||
|
icon: '委',
|
||||||
|
group: '组织奖项',
|
||||||
|
desc: '用于先进团委集体申报与材料汇总展示。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '五四红旗团支部',
|
||||||
|
code: 'gxmu_wshqtzb',
|
||||||
|
icon: '支',
|
||||||
|
group: '组织奖项',
|
||||||
|
desc: '聚焦先进团支部建设成果与品牌工作展示。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '优秀共青团干部',
|
||||||
|
code: 'gxmu_yxgqtdgb',
|
||||||
|
icon: '干',
|
||||||
|
group: '个人奖项',
|
||||||
|
desc: '面向团学骨干和团务干部的申报入口。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '优秀共青团员',
|
||||||
|
code: 'gxmu_yxgqty',
|
||||||
|
icon: '员',
|
||||||
|
group: '个人奖项',
|
||||||
|
desc: '面向优秀团员个人事迹与成长表现申报。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '挑战杯',
|
||||||
|
code: 'gxmu_tzbcy_form',
|
||||||
|
icon: '挑',
|
||||||
|
group: '竞赛项目',
|
||||||
|
desc: '面向挑战杯项目材料填报与过程归档。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '青马工程',
|
||||||
|
code: 'gxmu_qmgc_form',
|
||||||
|
icon: '青',
|
||||||
|
group: '培养项目',
|
||||||
|
desc: '用于青马工程学员培养信息和成果申报。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '未来学术之星',
|
||||||
|
code: 'gxmu_wxxzx_project',
|
||||||
|
icon: '星',
|
||||||
|
group: '创新项目',
|
||||||
|
desc: '用于未来学术之星项目申报和材料整理。'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const createFields = (fields) =>
|
||||||
|
fields.map((field) => ({
|
||||||
|
required: false,
|
||||||
|
placeholder: `请输入${field.label}`,
|
||||||
|
...field
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const GXMU_FORM_MAP = {
|
||||||
|
gxmu_sjqn: {
|
||||||
|
title: '十佳青年岗位能手申报表',
|
||||||
|
subtitle: '参考后台 SJQNForm 字段结构,适用于个人申报信息填写。',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: '基础信息',
|
||||||
|
fields: createFields([
|
||||||
|
{ key: 'year', label: '年份', type: 'year', required: true },
|
||||||
|
{ key: 'applyType', label: '申报类别', required: true },
|
||||||
|
{ key: 'name', label: '姓名', required: true },
|
||||||
|
{ key: 'gender', label: '性别', type: 'select', options: GENDER_OPTIONS },
|
||||||
|
{ key: 'birthMonth', label: '出生年月', type: 'month' },
|
||||||
|
{ key: 'nation', label: '民族' },
|
||||||
|
{ key: 'politics', label: '政治面貌' },
|
||||||
|
{ key: 'education', label: '学历' },
|
||||||
|
{ key: 'position', label: '职务' },
|
||||||
|
{ key: 'title', label: '职称' },
|
||||||
|
{ key: 'unit', label: '所在单位' }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '申报内容',
|
||||||
|
fields: createFields([
|
||||||
|
{ key: 'awards', label: '近三年曾获校级及以上奖励', type: 'textarea' },
|
||||||
|
{ key: 'experience', label: '工作(学习)经历', type: 'textarea' },
|
||||||
|
{ key: 'mainStory', label: '主要事迹', type: 'textarea' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
gxmu_sjtbzbsj: {
|
||||||
|
title: '十佳团支部书记申报表',
|
||||||
|
subtitle: '参考后台 SJTBZBSJForm 字段结构,适用于个人申报信息填写。',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: '基础信息',
|
||||||
|
fields: createFields([
|
||||||
|
{ key: 'year', label: '年份', type: 'year', required: true },
|
||||||
|
{ key: 'name', label: '姓名', required: true },
|
||||||
|
{ key: 'gender', label: '性别', type: 'select', options: GENDER_OPTIONS },
|
||||||
|
{ key: 'birthMonth', label: '出生年月', type: 'month' },
|
||||||
|
{ key: 'nation', label: '民族' },
|
||||||
|
{ key: 'politics', label: '政治面貌' },
|
||||||
|
{ key: 'collegeClass', label: '学院班级/单位科室' },
|
||||||
|
{ key: 'branch', label: '所在团支部' },
|
||||||
|
{ key: 'eduEval', label: '上一年度团员教育评议等次' }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '申报内容',
|
||||||
|
fields: createFields([
|
||||||
|
{ key: 'awards', label: '近三年曾获校级及以上奖励', type: 'textarea' },
|
||||||
|
{ key: 'experience', label: '工作(学习)经历', type: 'textarea' },
|
||||||
|
{ key: 'mainStory', label: '主要事迹', type: 'textarea' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
gxmu_wshqtw: {
|
||||||
|
title: '五四红旗团委申报表',
|
||||||
|
subtitle: '参考后台 WSHQTWForm 字段结构,适用于组织集体申报。',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: '组织基本信息',
|
||||||
|
fields: createFields([
|
||||||
|
{ key: 'year', label: '年份', type: 'year', required: true },
|
||||||
|
{ key: 'orgName', label: '二级团组织全称', required: true },
|
||||||
|
{ key: 'leader', label: '负责人' },
|
||||||
|
{ key: 'phone', label: '联系电话', type: 'phone' },
|
||||||
|
{ key: 'memberTotal', label: '现有团员总数', type: 'number' },
|
||||||
|
{ key: 'memberDeveloped2024', label: '上一年度发展团员人数', type: 'number' },
|
||||||
|
{ key: 'smartSystemLogin', label: '是否已登录智慧团建系统', type: 'select', options: YES_NO_OPTIONS },
|
||||||
|
{ key: 'committeeCount', label: '团委委员人数', type: 'number' },
|
||||||
|
{ key: 'fulltimeCadreCount', label: '专职团干部数(其中教师/学生)' },
|
||||||
|
{ key: 'parttimeCadreCount', label: '兼职团干部数(其中教师/学生)' },
|
||||||
|
{ key: 'lastElectionTime', label: '团委最近一次换届时间', type: 'date' },
|
||||||
|
{ key: 'branchCount', label: '团支部数', type: 'number' }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '年度建设情况',
|
||||||
|
fields: createFields([
|
||||||
|
{ key: 'feeReceivable2024', label: '上一年度应收团费', type: 'number' },
|
||||||
|
{ key: 'feeReceived2024', label: '上一年度实收团费', type: 'number' },
|
||||||
|
{ key: 'feePayable2024', label: '上一年度应上缴团费', type: 'number' },
|
||||||
|
{ key: 'feePaid2024', label: '上一年度实际上缴团费', type: 'number' },
|
||||||
|
{ key: 'standardizedWork2024', label: '是否开展规范化建设工作', type: 'select', options: YES_NO_OPTIONS },
|
||||||
|
{ key: 'recommendActivist2024', label: '推荐入党积极分子人数', type: 'number' },
|
||||||
|
{ key: 'activistConfirmed', label: '确定为入党积极分子数', type: 'number' },
|
||||||
|
{ key: 'recommendDevTarget2024', label: '推荐党的发展对象人数', type: 'number' },
|
||||||
|
{ key: 'devTargetConfirmed', label: '确定为党的发展对象数', type: 'number' }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '主要成果',
|
||||||
|
fields: createFields([
|
||||||
|
{ key: 'honorsFiveYears', label: '近五年获得校级及以上荣誉情况', type: 'textarea' },
|
||||||
|
{ key: 'workSummaryThreeYears', label: '近三年开展的主要工作及取得的效果', type: 'textarea' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
gxmu_wshqtzb: {
|
||||||
|
title: '五四红旗团支部申报表',
|
||||||
|
subtitle: '参考后台 WSHQTZBForm 字段结构,适用于基层团支部集体申报。',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: '组织基本信息',
|
||||||
|
fields: createFields([
|
||||||
|
{ key: 'year', label: '年份', type: 'year', required: true },
|
||||||
|
{ key: 'branchName', label: '团支部全称', required: true },
|
||||||
|
{ key: 'secondOrg', label: '所属二级团组织' },
|
||||||
|
{ key: 'secretary', label: '团支部书记' },
|
||||||
|
{ key: 'politics', label: '政治面貌' },
|
||||||
|
{ key: 'contact', label: '联系方式', type: 'phone' },
|
||||||
|
{ key: 'establishTime', label: '成立时间', type: 'date' },
|
||||||
|
{ key: 'lastElectionTime', label: '最近一次换届时间', type: 'date' },
|
||||||
|
{ key: 'smartSystemLogin', label: '是否已登录智慧团建系统', type: 'select', options: YES_NO_OPTIONS },
|
||||||
|
{ key: 'memberTotal', label: '现有团员总数', type: 'number' },
|
||||||
|
{ key: 'memberDeveloped2024', label: '上一年度发展团员数', type: 'number' }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '年度建设情况',
|
||||||
|
fields: createFields([
|
||||||
|
{ key: 'feeReceivable2024', label: '上一年度应收团费', type: 'number' },
|
||||||
|
{ key: 'feeReceived2024', label: '上一年度实收团费', type: 'number' },
|
||||||
|
{ key: 'feePayable2024', label: '上一年度应上缴团费', type: 'number' },
|
||||||
|
{ key: 'feePaid2024', label: '上一年度实际上缴团费', type: 'number' },
|
||||||
|
{ key: 'recommendActivist2024', label: '推荐入党积极分子人数', type: 'number' },
|
||||||
|
{ key: 'activistConfirmed', label: '确定为入党积极分子数', type: 'number' },
|
||||||
|
{ key: 'recommendDevTarget2024', label: '推荐党的发展对象人数', type: 'number' },
|
||||||
|
{ key: 'devTargetConfirmed', label: '确定为党的发展对象数', type: 'number' },
|
||||||
|
{ key: 'branchCommitteeMeetingCount', label: '团支部委员会会议召开次数', type: 'number' },
|
||||||
|
{ key: 'branchMemberMeetingCount', label: '团支部团员大会召开次数', type: 'number' },
|
||||||
|
{ key: 'eduEvalDone', label: '是否开展团员教育评议', type: 'select', options: YES_NO_OPTIONS },
|
||||||
|
{ key: 'annualRegDone', label: '是否开展团员年度团籍注册', type: 'select', options: YES_NO_OPTIONS },
|
||||||
|
{ key: 'classCount', label: '开展团课次数', type: 'number' },
|
||||||
|
{ key: 'smartSystem100', label: '是否100%录入智慧团建', type: 'select', options: YES_NO_OPTIONS }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '主要成果',
|
||||||
|
fields: createFields([
|
||||||
|
{ key: 'honorsFiveYears', label: '近五年获得院级及以上荣誉情况', type: 'textarea' },
|
||||||
|
{ key: 'workSummaryThreeYears', label: '近三年开展的主要工作及取得的效果', type: 'textarea' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
gxmu_yxgqtdgb: {
|
||||||
|
title: '优秀共青团干部申报表',
|
||||||
|
subtitle: '参考后台 YXGQTDGBForm 字段结构,适用于团干部个人申报。',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: '基础信息',
|
||||||
|
fields: createFields([
|
||||||
|
{ key: 'year', label: '年份', type: 'year', required: true },
|
||||||
|
{ key: 'name', label: '姓名', required: true },
|
||||||
|
{ key: 'gender', label: '性别', type: 'select', options: GENDER_OPTIONS },
|
||||||
|
{ key: 'nation', label: '民族' },
|
||||||
|
{ key: 'birthMonth', label: '出生年月', type: 'month' },
|
||||||
|
{ key: 'politics', label: '政治面貌' },
|
||||||
|
{ key: 'position', label: '职务' },
|
||||||
|
{ key: 'identity', label: '身份' },
|
||||||
|
{ key: 'organization', label: '所在团组织' },
|
||||||
|
{ key: 'contact', label: '联系方式', type: 'phone' },
|
||||||
|
{ key: 'memberNo', label: '发展团员编号' },
|
||||||
|
{ key: 'currentDutyTime', label: '任现团内职务时间', type: 'month' },
|
||||||
|
{ key: 'cadreYears', label: '担任团干部年限' },
|
||||||
|
{ key: 'assessment2024', label: '上一年度工作考核结果' },
|
||||||
|
{ key: 'volunteerRegTime', label: '成为注册志愿者时间', type: 'month' }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '申报内容',
|
||||||
|
fields: createFields([
|
||||||
|
{ key: 'cadreExperience', label: '从事团干部经历', type: 'textarea' },
|
||||||
|
{ key: 'honorsFiveYears', label: '近五年获得校级及以上荣誉情况', type: 'textarea' },
|
||||||
|
{ key: 'mainStory', label: '主要事迹', type: 'textarea' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
gxmu_yxgqty: {
|
||||||
|
title: '优秀共青团员申报表',
|
||||||
|
subtitle: '参考后台 YXGQTYForm 字段结构,适用于团员个人申报。',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: '基础信息',
|
||||||
|
fields: createFields([
|
||||||
|
{ key: 'year', label: '年份', type: 'year', required: true },
|
||||||
|
{ key: 'name', label: '姓名', required: true },
|
||||||
|
{ key: 'gender', label: '性别', type: 'select', options: GENDER_OPTIONS },
|
||||||
|
{ key: 'nation', label: '民族' },
|
||||||
|
{ key: 'politics', label: '政治面貌' },
|
||||||
|
{ key: 'birthMonth', label: '出生年月', type: 'month' },
|
||||||
|
{ key: 'joinTime', label: '入团时间', type: 'month' },
|
||||||
|
{ key: 'collegeMajorClass', label: '所在学院、专业、班级' },
|
||||||
|
{ key: 'position', label: '职务' },
|
||||||
|
{ key: 'volunteerRegTime', label: '成为注册志愿者时间', type: 'month' },
|
||||||
|
{ key: 'eduEval2024', label: '上一年度团员教育评议等次' },
|
||||||
|
{ key: 'smartSystem', label: '是否已登录智慧团建系统', type: 'select', options: YES_NO_OPTIONS },
|
||||||
|
{ key: 'contact', label: '联系电话', type: 'phone' },
|
||||||
|
{ key: 'totalVolunteerHours', label: '累计志愿服务时长' },
|
||||||
|
{ key: 'volunteerHours2024', label: '上一年度志愿服务时长' },
|
||||||
|
{ key: 'memberNo', label: '发展团员编号' }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '申报内容',
|
||||||
|
fields: createFields([
|
||||||
|
{ key: 'honorsFiveYears', label: '近五年获得荣誉情况', type: 'textarea' },
|
||||||
|
{ key: 'mainStory', label: '主要事迹', type: 'textarea' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
gxmu_tzbcy_form: {
|
||||||
|
title: '挑战杯申报表',
|
||||||
|
subtitle: '面向挑战杯项目申报、团队信息和成果材料整理。',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: '项目信息',
|
||||||
|
fields: createFields([
|
||||||
|
{ key: 'year', label: '年份', type: 'year', required: true },
|
||||||
|
{ key: 'projectName', label: '项目名称', required: true },
|
||||||
|
{ key: 'projectType', label: '项目类型', required: true },
|
||||||
|
{ key: 'projectGroup', label: '项目分组', required: true },
|
||||||
|
{ key: 'leaderName', label: '项目负责人', required: true },
|
||||||
|
{ key: 'leaderCollege', label: '负责人学院' },
|
||||||
|
{ key: 'leaderPhone', label: '负责人电话', type: 'phone' },
|
||||||
|
{ key: 'guidanceTeacher', label: '指导老师' }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '项目内容',
|
||||||
|
fields: createFields([
|
||||||
|
{ key: 'projectSummary', label: '项目简介', type: 'textarea', required: true },
|
||||||
|
{ key: 'innovationPoint', label: '创新亮点', type: 'textarea' },
|
||||||
|
{ key: 'teamMembers', label: '团队成员', type: 'textarea' },
|
||||||
|
{ key: 'honors', label: '已有成果与奖励', type: 'textarea' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
gxmu_qmgc_form: {
|
||||||
|
title: '青马工程申报表',
|
||||||
|
subtitle: '用于青马工程学员基础信息、培养情况和成果申报。',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: '基础信息',
|
||||||
|
fields: createFields([
|
||||||
|
{ key: 'year', label: '年份', type: 'year', required: true },
|
||||||
|
{ key: 'name', label: '姓名', required: true },
|
||||||
|
{ key: 'gender', label: '性别', type: 'select', options: GENDER_OPTIONS },
|
||||||
|
{ key: 'college', label: '学院', required: true },
|
||||||
|
{ key: 'className', label: '班级' },
|
||||||
|
{ key: 'phone', label: '联系电话', type: 'phone' },
|
||||||
|
{ key: 'politics', label: '政治面貌' }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '培养情况',
|
||||||
|
fields: createFields([
|
||||||
|
{ key: 'trainingExperience', label: '培养经历', type: 'textarea', required: true },
|
||||||
|
{ key: 'practiceExperience', label: '实践经历', type: 'textarea' },
|
||||||
|
{ key: 'mainAchievement', label: '主要成果', type: 'textarea' },
|
||||||
|
{ key: 'selfEvaluation', label: '个人总结', type: 'textarea' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
gxmu_wxxzx_project: {
|
||||||
|
title: '未来学术之星申报表',
|
||||||
|
subtitle: '用于未来学术之星项目申报、课题信息和支撑材料整理。',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: '项目基础信息',
|
||||||
|
fields: createFields([
|
||||||
|
{ key: 'year', label: '年份', type: 'year', required: true },
|
||||||
|
{ key: 'projectName', label: '项目名称', required: true },
|
||||||
|
{ key: 'applicantName', label: '申请人', required: true },
|
||||||
|
{ key: 'college', label: '学院', required: true },
|
||||||
|
{ key: 'major', label: '专业' },
|
||||||
|
{ key: 'phone', label: '联系电话', type: 'phone' },
|
||||||
|
{ key: 'guidanceTeacher', label: '指导老师' }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '申报内容',
|
||||||
|
fields: createFields([
|
||||||
|
{ key: 'researchDirection', label: '研究方向', type: 'textarea' },
|
||||||
|
{ key: 'projectBasis', label: '项目基础', type: 'textarea', required: true },
|
||||||
|
{ key: 'researchPlan', label: '研究计划', type: 'textarea' },
|
||||||
|
{ key: 'expectedResult', label: '预期成果', type: 'textarea' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getModuleMeta = (code) => GXMU_MODULES.find((item) => item.code === code)
|
||||||
|
|
||||||
|
export const getFormConfig = (code) => GXMU_FORM_MAP[code]
|
||||||
|
|
||||||
|
export const createInitialFormData = (code) => {
|
||||||
|
const config = getFormConfig(code)
|
||||||
|
if (!config) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
return config.sections.reduce((result, section) => {
|
||||||
|
section.fields.forEach((field) => {
|
||||||
|
result[field.key] = field.defaultValue || ''
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
886
pages/gxmu/cross-school-board.vue
Normal file
@@ -0,0 +1,886 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<scroll-view scroll-y class="page-scroll">
|
||||||
|
<common-hero title="跨校活动情报看板" :use-image-bg="true"></common-hero>
|
||||||
|
<view class="hero-panel">
|
||||||
|
<view class="hero-copy">
|
||||||
|
<view class="hero-kicker">跨校活动情报看板</view>
|
||||||
|
<view class="hero-title">把兄弟高校的活动打法,整理成一眼可读的案例板</view>
|
||||||
|
<view class="hero-desc">
|
||||||
|
以信息流和案例卡片形式集中展示同类活动的公开线索,方便快速判断选题趋势、包装方式和传播动作。
|
||||||
|
</view>
|
||||||
|
<view class="hero-actions">
|
||||||
|
<view class="hero-btn hero-btn--primary" @tap="quickFilter('高热')">本周热点</view>
|
||||||
|
<view class="hero-btn" @tap="quickMatchPlanning">拟办活动对标</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="hero-stats">
|
||||||
|
<view class="hero-stat hero-stat--primary">
|
||||||
|
<view class="stat-label">案例总量</view>
|
||||||
|
<view class="stat-value">{{ totalCount }}</view>
|
||||||
|
<view class="stat-desc">当前静态示例数据</view>
|
||||||
|
</view>
|
||||||
|
<view class="hero-stat">
|
||||||
|
<view class="stat-label">高热案例</view>
|
||||||
|
<view class="stat-value">{{ highHeatCount }}</view>
|
||||||
|
<view class="stat-desc">适合优先拆解传播路径</view>
|
||||||
|
</view>
|
||||||
|
<view class="hero-stat">
|
||||||
|
<view class="stat-label">覆盖类型</view>
|
||||||
|
<view class="stat-value">{{ categoryCount }}</view>
|
||||||
|
<view class="stat-desc">品牌活动 / 实践 / 科创 / 文体</view>
|
||||||
|
</view>
|
||||||
|
<view class="hero-stat">
|
||||||
|
<view class="stat-label">来源院校</view>
|
||||||
|
<view class="stat-value">{{ schoolCount }}</view>
|
||||||
|
<view class="stat-desc">按院校公开渠道聚合</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="filter-panel">
|
||||||
|
<view class="filter-group">
|
||||||
|
<view class="filter-label">活动类型</view>
|
||||||
|
<scroll-view scroll-x class="chip-scroll" show-scrollbar="false">
|
||||||
|
<view class="chip-row">
|
||||||
|
<view
|
||||||
|
v-for="item in categoryOptions"
|
||||||
|
:key="item"
|
||||||
|
class="filter-chip"
|
||||||
|
:class="{ 'filter-chip--active': selectedCategory === item }"
|
||||||
|
@tap="selectedCategory = item"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="filter-group">
|
||||||
|
<view class="filter-label">热度等级</view>
|
||||||
|
<scroll-view scroll-x class="chip-scroll" show-scrollbar="false">
|
||||||
|
<view class="chip-row">
|
||||||
|
<view
|
||||||
|
v-for="item in heatOptions"
|
||||||
|
:key="item"
|
||||||
|
class="filter-chip"
|
||||||
|
:class="{ 'filter-chip--active': selectedHeat === item }"
|
||||||
|
@tap="selectedHeat = item"
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="search-box">
|
||||||
|
<uv-input
|
||||||
|
v-model.trim="keyword"
|
||||||
|
class="search-input"
|
||||||
|
placeholder="搜索标题、院校、标签"
|
||||||
|
confirm-type="search"
|
||||||
|
:maxlength="-1"
|
||||||
|
border="none"
|
||||||
|
/>
|
||||||
|
<view v-if="keyword" class="search-clear" @tap="keyword = ''">清空</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section-grid">
|
||||||
|
<view class="stream-card">
|
||||||
|
<view class="section-title">情报信息流</view>
|
||||||
|
<view class="stream-list">
|
||||||
|
<view
|
||||||
|
v-for="item in streamItems"
|
||||||
|
:key="item.id"
|
||||||
|
class="stream-item"
|
||||||
|
>
|
||||||
|
<view class="stream-cover" :class="coverClassMap[item.id % coverClassMap.length]">
|
||||||
|
<view class="stream-cover-category">{{ item.category }}</view>
|
||||||
|
<view class="stream-cover-school">{{ item.school }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="stream-content">
|
||||||
|
<view class="stream-top">
|
||||||
|
<view class="stream-title">{{ item.title }}</view>
|
||||||
|
<view class="heat-tag" :class="heatClassMap[item.heat]">{{ item.heat }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="stream-meta">{{ item.school }} · {{ item.source }} · {{ item.publishAt }}</view>
|
||||||
|
<view class="stream-summary">{{ item.summary }}</view>
|
||||||
|
<view class="tag-list">
|
||||||
|
<view v-for="tag in item.tags" :key="tag" class="tag-chip">{{ tag }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="signal-card">
|
||||||
|
<view class="section-title">策略信号</view>
|
||||||
|
<view class="signal-list">
|
||||||
|
<view v-for="item in insightSignals" :key="item.title" class="signal-item">
|
||||||
|
<view class="signal-title">{{ item.title }}</view>
|
||||||
|
<view class="signal-desc">{{ item.desc }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="signal-divider"></view>
|
||||||
|
<view class="section-title section-title--small">建议关注</view>
|
||||||
|
<view class="watch-list">
|
||||||
|
<view v-for="item in watchList" :key="item" class="watch-item">{{ item }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view id="cases-section" class="cases-card">
|
||||||
|
<view class="cases-head">
|
||||||
|
<view class="section-title">案例卡片墙</view>
|
||||||
|
<view class="case-count">共 {{ filteredCases.length }} 条</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="filteredCases.length" class="cases-list">
|
||||||
|
<view
|
||||||
|
v-for="item in filteredCases"
|
||||||
|
:key="item.id"
|
||||||
|
class="case-card"
|
||||||
|
>
|
||||||
|
<view class="case-cover" :class="coverClassMap[item.id % coverClassMap.length]">
|
||||||
|
<view class="case-school">{{ item.school }}</view>
|
||||||
|
<view class="case-cover-badge">{{ item.highlight }}</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="case-body">
|
||||||
|
<view class="case-head">
|
||||||
|
<view class="case-tag">{{ item.category }}</view>
|
||||||
|
<view class="heat-tag" :class="heatClassMap[item.heat]">{{ item.heat }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="case-title">{{ item.title }}</view>
|
||||||
|
<view class="case-summary">{{ item.summary }}</view>
|
||||||
|
<view class="case-info">来源:{{ item.source }}</view>
|
||||||
|
<view class="case-info">发布时间:{{ item.publishAt }}</view>
|
||||||
|
<view class="tag-list">
|
||||||
|
<view v-for="tag in item.tags" :key="tag" class="tag-chip"># {{ tag }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="case-footer">
|
||||||
|
<view class="signal-badge">
|
||||||
|
<view class="signal-badge-label">可借鉴动作</view>
|
||||||
|
<view class="signal-badge-value">{{ item.highlight }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="origin-link" @tap="handleOpenLink(item)">查看原文</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else class="empty-card">暂无匹配的案例</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CommonHero from '../../components/common-hero/common-hero.vue'
|
||||||
|
const CASES = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '青年宣讲训练营联合短视频挑战赛',
|
||||||
|
school: '华东某高校团委',
|
||||||
|
category: '品牌活动',
|
||||||
|
heat: '高热',
|
||||||
|
source: '校团委公众号',
|
||||||
|
publishAt: '03-26 18:20',
|
||||||
|
summary:
|
||||||
|
'以“训练营 + 路演 + 短视频共创”三段式推进,先做骨干培训,再通过挑战赛扩散校园参与度,适合复制到主题教育和品牌宣讲场景。',
|
||||||
|
tags: ['宣讲', '短视频', '路演', '骨干培训'],
|
||||||
|
link: 'https://example.com/cross-school/case-01',
|
||||||
|
highlight: '训练营 + 二创传播'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: '劳动教育月采用主会场直播联动分会场打卡',
|
||||||
|
school: '华南某高校学生会',
|
||||||
|
category: '实践活动',
|
||||||
|
heat: '跟进',
|
||||||
|
source: '青春校园网',
|
||||||
|
publishAt: '03-25 09:40',
|
||||||
|
summary:
|
||||||
|
'通过主会场直播统一调性,再让各学院自带话题完成线下打卡,既保留主视觉控制力,也扩大二级学院参与面。',
|
||||||
|
tags: ['劳动教育', '直播', '学院联动'],
|
||||||
|
link: 'https://example.com/cross-school/case-02',
|
||||||
|
highlight: '直播总控 + 分会场扩散'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: '科创竞赛经验周报做成案例地图与备赛清单',
|
||||||
|
school: '华中某高校创新中心',
|
||||||
|
category: '科创竞赛',
|
||||||
|
heat: '高热',
|
||||||
|
source: '双创平台',
|
||||||
|
publishAt: '03-24 21:15',
|
||||||
|
summary:
|
||||||
|
'不只发布赛事通知,而是补充往届项目地图、导师方向、材料清单和时间线,显著降低学生获取备赛信息的门槛。',
|
||||||
|
tags: ['挑战杯', '项目库', '备赛', '知识沉淀'],
|
||||||
|
link: 'https://example.com/cross-school/case-03',
|
||||||
|
highlight: '赛事通知升级为攻略型内容'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: '校园歌会从报名表单升级为城市主题内容企划',
|
||||||
|
school: '西南某高校团委',
|
||||||
|
category: '文体活动',
|
||||||
|
heat: '观察',
|
||||||
|
source: '学校新闻网',
|
||||||
|
publishAt: '03-23 16:05',
|
||||||
|
summary:
|
||||||
|
'围绕城市记忆与青年表达设置主题分场,通过海报、话题征集和观众投票提前积累内容资产,活动尚未开始就形成讨论度。',
|
||||||
|
tags: ['歌会', '主题策划', '投票互动'],
|
||||||
|
link: 'https://example.com/cross-school/case-04',
|
||||||
|
highlight: '先做内容议题再做活动报名'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: '社会实践出征仪式叠加成果展和队伍名片墙',
|
||||||
|
school: '西北某高校实践中心',
|
||||||
|
category: '实践活动',
|
||||||
|
heat: '跟进',
|
||||||
|
source: '实践育人专栏',
|
||||||
|
publishAt: '03-22 11:30',
|
||||||
|
summary:
|
||||||
|
'将传统仪式升级为“成果展 + 队伍画像 + 媒体素材包”组合,便于后续持续发布实践过程稿和回访报道。',
|
||||||
|
tags: ['社会实践', '成果展', '队伍画像'],
|
||||||
|
link: 'https://example.com/cross-school/case-05',
|
||||||
|
highlight: '出征现场即完成素材采集'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
title: '青马工程读书班采用任务制和周主题海报并行',
|
||||||
|
school: '东北某高校马克思主义学院',
|
||||||
|
category: '品牌活动',
|
||||||
|
heat: '高热',
|
||||||
|
source: '学院团学平台',
|
||||||
|
publishAt: '03-21 20:10',
|
||||||
|
summary:
|
||||||
|
'将阅读、研讨、海报共创和阶段展示拆成清晰任务包,持续释放可视化成果,比单次讲座更容易维持项目关注度。',
|
||||||
|
tags: ['青马工程', '阅读打卡', '任务制'],
|
||||||
|
link: 'https://example.com/cross-school/case-06',
|
||||||
|
highlight: '过程任务化,成果可视化'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const INSIGHT_SIGNALS = [
|
||||||
|
{
|
||||||
|
title: '“活动报名”正在转向“内容预热”',
|
||||||
|
desc: '高热案例普遍会在报名期前先释放主题海报、人物故事或任务线索,先做认知再做转化。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '学院联动成为扩大参与面的主流方式',
|
||||||
|
desc: '不少活动不再单点发通知,而是设计统一主视觉和话题标签,把执行动作分发给二级学院。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '成果展示正在前置',
|
||||||
|
desc: '从出征仪式到训练营,多数案例会在启动阶段同步准备成果墙、名片墙或可转发素材。'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const WATCH_LIST = [
|
||||||
|
'活动是否配置统一 hashtag 与二创任务',
|
||||||
|
'是否提供报名后自动领取的素材包',
|
||||||
|
'是否能把案例沉淀为往届经验库',
|
||||||
|
'是否区分预热、爆发、回顾三个传播阶段'
|
||||||
|
]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CommonHero
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
categoryOptions: ['全部', '品牌活动', '实践活动', '科创竞赛', '文体活动'],
|
||||||
|
heatOptions: ['全部', '高热', '跟进', '观察'],
|
||||||
|
selectedCategory: '全部',
|
||||||
|
selectedHeat: '全部',
|
||||||
|
keyword: '',
|
||||||
|
cases: CASES,
|
||||||
|
insightSignals: INSIGHT_SIGNALS,
|
||||||
|
watchList: WATCH_LIST,
|
||||||
|
coverClassMap: ['cover-a', 'cover-b', 'cover-c', 'cover-d'],
|
||||||
|
heatClassMap: {
|
||||||
|
高热: 'heat-tag--hot',
|
||||||
|
跟进: 'heat-tag--follow',
|
||||||
|
观察: 'heat-tag--watch'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filteredCases() {
|
||||||
|
const text = String(this.keyword || '').trim().toLowerCase()
|
||||||
|
return this.cases.filter((item) => {
|
||||||
|
const matchCategory = this.selectedCategory === '全部' || item.category === this.selectedCategory
|
||||||
|
const matchHeat = this.selectedHeat === '全部' || item.heat === this.selectedHeat
|
||||||
|
const searchText = [item.title, item.school, item.summary, item.tags.join(' ')].join(' ').toLowerCase()
|
||||||
|
return matchCategory && matchHeat && (!text || searchText.includes(text))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
totalCount() {
|
||||||
|
return this.cases.length
|
||||||
|
},
|
||||||
|
highHeatCount() {
|
||||||
|
return this.cases.filter((item) => item.heat === '高热').length
|
||||||
|
},
|
||||||
|
categoryCount() {
|
||||||
|
return new Set(this.cases.map((item) => item.category)).size
|
||||||
|
},
|
||||||
|
schoolCount() {
|
||||||
|
return new Set(this.cases.map((item) => item.school)).size
|
||||||
|
},
|
||||||
|
streamItems() {
|
||||||
|
return this.filteredCases.slice(0, 4)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
quickFilter(heat) {
|
||||||
|
this.selectedHeat = heat
|
||||||
|
},
|
||||||
|
quickMatchPlanning() {
|
||||||
|
this.selectedCategory = '品牌活动'
|
||||||
|
this.selectedHeat = '高热'
|
||||||
|
uni.showToast({
|
||||||
|
title: '已切换到高热品牌活动案例',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleOpenLink(item) {
|
||||||
|
if (!item || !item.link) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '原文链接待接入',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: item.link,
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '原文链接已复制',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(80, 180, 255, 0.18), transparent 28%),
|
||||||
|
linear-gradient(180deg, #f5fbff 0%, #f5fbff 36%, #fff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-scroll {
|
||||||
|
height: 100vh;
|
||||||
|
//padding: 28rpx 24rpx 40rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-panel {
|
||||||
|
padding: 30rpx;
|
||||||
|
border-radius: 30rpx;
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(255, 255, 255, 0.94), rgba(245, 251, 255, 0.94)),
|
||||||
|
repeating-linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(21, 144, 255, 0.04) 0,
|
||||||
|
rgba(21, 144, 255, 0.04) 2rpx,
|
||||||
|
transparent 2rpx,
|
||||||
|
transparent 30rpx
|
||||||
|
);
|
||||||
|
box-shadow: 0 16rpx 48rpx rgba(21, 144, 255, 0.1);
|
||||||
|
border: 1rpx solid rgba(21, 144, 255, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-kicker {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(21, 144, 255, 0.08);
|
||||||
|
color: #1496f2;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
margin-top: 22rpx;
|
||||||
|
color: #1f2d3d;
|
||||||
|
font-size: 44rpx;
|
||||||
|
line-height: 1.3;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-desc {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
color: #5f7f9f;
|
||||||
|
font-size: 25rpx;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-btn {
|
||||||
|
padding: 16rpx 24rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #f3f9ff;
|
||||||
|
border: 1rpx solid rgba(21, 144, 255, 0.16);
|
||||||
|
color: #1496f2;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-btn--primary {
|
||||||
|
background: linear-gradient(135deg, #32bfff, #1496f2);
|
||||||
|
color: #fff;
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: 0 10rpx 26rpx rgba(21, 144, 255, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stat {
|
||||||
|
padding: 22rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
border: 1rpx solid rgba(21, 144, 255, 0.12);
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(21, 144, 255, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stat--primary {
|
||||||
|
background: linear-gradient(160deg, #32bfff 0%, #1496f2 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stat--primary .stat-label,
|
||||||
|
.hero-stat--primary .stat-desc {
|
||||||
|
color: rgba(255, 255, 255, 0.84);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: #5f7f9f;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
margin: 10rpx 0 8rpx;
|
||||||
|
font-size: 48rpx;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-desc {
|
||||||
|
color: #5f7f9f;
|
||||||
|
font-size: 21rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-alert,
|
||||||
|
.filter-panel,
|
||||||
|
.stream-card,
|
||||||
|
.signal-card,
|
||||||
|
.cases-card {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
box-shadow: 0 16rpx 40rpx rgba(21, 144, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-alert {
|
||||||
|
border: 1rpx solid rgba(21, 144, 255, 0.18);
|
||||||
|
background: rgba(255, 251, 248, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-alert-title {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2d3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-alert-desc {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
font-size: 23rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #5f7f9f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group + .filter-group {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-label {
|
||||||
|
color: #5f7f9f;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-scroll {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 14rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chip-row {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding-right: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10rpx 20rpx;
|
||||||
|
border: 1rpx solid rgba(21, 144, 255, 0.16);
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #f5fbff;
|
||||||
|
color: #1496f2;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chip--active {
|
||||||
|
color: #fff;
|
||||||
|
border-color: transparent;
|
||||||
|
background: linear-gradient(135deg, #32bfff, #1496f2);
|
||||||
|
box-shadow: 0 8rpx 20rpx rgba(21, 144, 255, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
padding: 0 18rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #f8fbff;
|
||||||
|
border: 1rpx solid rgba(21, 144, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.search-input.uv-input) {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.search-input.uv-input .uv-input__content__field-wrapper__field) {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #1f2d3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-clear {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #1496f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-grid {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2d3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title--small {
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-list,
|
||||||
|
.signal-list,
|
||||||
|
.watch-list,
|
||||||
|
.cases-list {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-item,
|
||||||
|
.signal-item,
|
||||||
|
.case-card,
|
||||||
|
.watch-item {
|
||||||
|
border: 1rpx solid rgba(21, 144, 255, 0.1);
|
||||||
|
background: linear-gradient(180deg, #f5fbff, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-item {
|
||||||
|
padding: 18rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-item + .stream-item,
|
||||||
|
.signal-item + .signal-item,
|
||||||
|
.watch-item + .watch-item,
|
||||||
|
.case-card + .case-card {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-cover,
|
||||||
|
.case-cover {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-cover {
|
||||||
|
height: 180rpx;
|
||||||
|
padding: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-cover {
|
||||||
|
height: 220rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-a {
|
||||||
|
background: linear-gradient(135deg, #9be7ff 0%, #32bfff 52%, #1496f2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-b {
|
||||||
|
background: linear-gradient(135deg, #c7f9cc 0%, #57cc99 52%, #38a3a5 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-c {
|
||||||
|
background: linear-gradient(135deg, #ffd6a5 0%, #ffadad 50%, #ff7b7b 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-d {
|
||||||
|
background: linear-gradient(135deg, #cdb4db 0%, #a2d2ff 52%, #8ecae6 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-cover::after,
|
||||||
|
.case-cover::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: -40rpx;
|
||||||
|
top: -40rpx;
|
||||||
|
width: 180rpx;
|
||||||
|
height: 180rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-cover-category,
|
||||||
|
.stream-cover-school,
|
||||||
|
.case-school,
|
||||||
|
.case-cover-badge {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-cover-category,
|
||||||
|
.case-cover-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 8rpx 14rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-cover-school,
|
||||||
|
.case-school {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-content,
|
||||||
|
.case-body {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-top,
|
||||||
|
.case-head,
|
||||||
|
.cases-head,
|
||||||
|
.case-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-title,
|
||||||
|
.case-title,
|
||||||
|
.signal-title {
|
||||||
|
color: #1f2d3d;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-title,
|
||||||
|
.case-title {
|
||||||
|
font-size: 29rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-meta,
|
||||||
|
.case-info,
|
||||||
|
.signal-desc,
|
||||||
|
.watch-item,
|
||||||
|
.stream-summary,
|
||||||
|
.case-summary {
|
||||||
|
color: #5f7f9f;
|
||||||
|
font-size: 23rpx;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-meta {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-summary,
|
||||||
|
.case-summary {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
color: #1f2d3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10rpx;
|
||||||
|
margin-top: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chip,
|
||||||
|
.case-tag,
|
||||||
|
.heat-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6rpx 14rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chip,
|
||||||
|
.case-tag {
|
||||||
|
background: #e8f4ff;
|
||||||
|
color: #1496f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heat-tag {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heat-tag--hot {
|
||||||
|
background: #fff1f0;
|
||||||
|
color: #e34d59;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heat-tag--follow {
|
||||||
|
background: #eef6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heat-tag--watch {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-item,
|
||||||
|
.watch-item {
|
||||||
|
padding: 18rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-desc {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-divider {
|
||||||
|
height: 1rpx;
|
||||||
|
margin: 20rpx 0;
|
||||||
|
background: rgba(21, 144, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-count {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #5f7f9f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-card {
|
||||||
|
border-radius: 26rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-cover-badge {
|
||||||
|
position: absolute;
|
||||||
|
left: 20rpx;
|
||||||
|
bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-body {
|
||||||
|
padding: 0 20rpx 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-title {
|
||||||
|
margin-top: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-info {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-badge {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 14rpx 18rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
background: #f7fbff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-badge-label {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #7b8794;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-badge-value {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2d3d;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.origin-link {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-top: 18rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1496f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-card {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
padding: 30rpx 24rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #f8fbff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #90a0b7;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
10
pages/gxmu/declare-service.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { API_BASE_URL } from '../../utils/request'
|
||||||
|
import { apiRequest } from '../assistant/chat-service'
|
||||||
|
|
||||||
|
export function listDeclare(params) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${API_BASE_URL}/gxmu/declare`,
|
||||||
|
method: 'GET',
|
||||||
|
data: params
|
||||||
|
})
|
||||||
|
}
|
||||||
892
pages/gxmu/form.vue
Normal file
@@ -0,0 +1,892 @@
|
|||||||
|
<template>
|
||||||
|
<view v-if="formConfig" class="page">
|
||||||
|
<scroll-view scroll-y class="page-scroll">
|
||||||
|
<common-hero :title="pageTitle" :use-image-bg="true"></common-hero>
|
||||||
|
<view class="hero-wrap">
|
||||||
|
<view class="hero-card">
|
||||||
|
<view class="hero-title">{{ pageTitle }}</view>
|
||||||
|
<view class="hero-desc">{{ moduleMeta && moduleMeta.desc ? moduleMeta.desc : '按模块维护申报记录,支持新增、编辑和提交。' }}</view>
|
||||||
|
<view class="hero-actions">
|
||||||
|
<view class="hero-action" @tap="openCreate">立即申报</view>
|
||||||
|
<view class="hero-action hero-action--secondary" @tap="loadData">刷新列表</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading" class="state-card">
|
||||||
|
<view class="state-title">正在加载申报记录...</view>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="!list.length" class="state-card">
|
||||||
|
<view class="state-title">暂无申报记录</view>
|
||||||
|
<view class="state-desc">点击上方“立即申报”开始填写当前模块。</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="card-list">
|
||||||
|
<view v-for="item in list" :key="item.id" class="data-card" @tap="openEdit(item)">
|
||||||
|
<view class="card-top">
|
||||||
|
<view>
|
||||||
|
<view class="card-title">{{ getRecordTitle(item) }}</view>
|
||||||
|
<view class="card-subtitle">{{ getRecordSubtitle(item) }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="card-year">{{ item.year || fixedYear || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-line">{{ getRecordMeta(item) }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view v-if="showEdit" class="modal-mask" @tap="closeEdit">
|
||||||
|
<view class="modal-panel" @tap.stop>
|
||||||
|
<view class="modal-head">
|
||||||
|
<view class="modal-title">{{ formData.id ? `编辑${pageTitle}` : `新增${pageTitle}` }}</view>
|
||||||
|
<view class="modal-close" @tap="closeEdit">关闭</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="modal-body">
|
||||||
|
<view class="hero-meta hero-meta--form">
|
||||||
|
<view class="meta-item meta-item--form">
|
||||||
|
<view class="meta-label meta-label--form">申报年度</view>
|
||||||
|
<view class="meta-value meta-value--form">{{ fixedYear || formData.year || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-item meta-item--form">
|
||||||
|
<view class="meta-label meta-label--form">草稿状态</view>
|
||||||
|
<view class="meta-value meta-value--form">{{ draftSavedAt ? '已保存' : '未保存' }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-for="section in formConfig.sections" :key="section.title" class="section-card">
|
||||||
|
<view class="section-head">
|
||||||
|
<view class="section-title">{{ section.title }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="field-list">
|
||||||
|
<view v-for="field in section.fields" :key="field.key" class="field-item">
|
||||||
|
<view class="field-label">
|
||||||
|
{{ field.label }}
|
||||||
|
<text v-if="field.required" class="field-required">*</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
v-if="field.type === 'textarea'"
|
||||||
|
class="field-textarea"
|
||||||
|
:placeholder="field.placeholder"
|
||||||
|
:maxlength="-1"
|
||||||
|
:value="formData[field.key]"
|
||||||
|
@input="handleInput(field, $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<view
|
||||||
|
v-else-if="isGenderField(field)"
|
||||||
|
class="field-picker"
|
||||||
|
@tap="openGenderPicker(field)"
|
||||||
|
>
|
||||||
|
<text :class="formData[field.key] ? 'field-value' : 'field-placeholder'">
|
||||||
|
{{ formData[field.key] || field.placeholder }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
v-else-if="isNationField(field)"
|
||||||
|
class="field-item-picker-wrap"
|
||||||
|
>
|
||||||
|
<picker
|
||||||
|
mode="selector"
|
||||||
|
:range="nationLabels"
|
||||||
|
:value="getNationIndex(field)"
|
||||||
|
:disabled="!nationOptions.length"
|
||||||
|
@change="handleNationPickerChange(field, $event)"
|
||||||
|
>
|
||||||
|
<view class="field-picker" :class="{ 'field-picker--disabled': !nationOptions.length }">
|
||||||
|
<text :class="formData[field.key] ? 'field-value' : 'field-placeholder'">
|
||||||
|
{{ formData[field.key] || (nationOptions.length ? '请选择民族' : '民族加载中') }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
v-else-if="isUvPickerField(field)"
|
||||||
|
class="field-picker"
|
||||||
|
:class="{ 'field-picker--disabled': isPickerFieldDisabled(field) }"
|
||||||
|
@tap="openSelectPicker(field)"
|
||||||
|
>
|
||||||
|
<text :class="formData[field.key] ? 'field-value' : 'field-placeholder'">
|
||||||
|
{{ getPickerDisplayText(field) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
v-else-if="isDateField(field)"
|
||||||
|
class="field-picker"
|
||||||
|
@tap="openDatePicker(field)"
|
||||||
|
>
|
||||||
|
<text :class="formData[field.key] ? 'field-value' : 'field-placeholder'">
|
||||||
|
{{ formData[field.key] || getDatePlaceholder(field) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<uv-input
|
||||||
|
v-else
|
||||||
|
class="field-input"
|
||||||
|
:modelValue="formData[field.key]"
|
||||||
|
:type="getInputType(field)"
|
||||||
|
:placeholder="field.placeholder"
|
||||||
|
placeholder-style="color: #c0c4cc;"
|
||||||
|
:maxlength="-1"
|
||||||
|
@input="setFieldValue(field.key, $event)"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view class="modal-actions">
|
||||||
|
<view class="ghost-btn" @tap="saveDraft">保存草稿</view>
|
||||||
|
<view class="primary-btn" @tap="submitForm">提交申报</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="empty-page">
|
||||||
|
<common-hero :title="pageTitle || '申报表'" :use-image-bg="true"></common-hero>
|
||||||
|
<view class="empty-body">
|
||||||
|
<view class="empty-title">未找到对应表单</view>
|
||||||
|
<view class="empty-desc">请返回上一页重新选择申报模块。</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<uv-picker
|
||||||
|
ref="genderPicker"
|
||||||
|
title="选择性别"
|
||||||
|
:columns="genderPickerColumns"
|
||||||
|
:defaultIndex="[genderPickerIndex]"
|
||||||
|
keyName="text"
|
||||||
|
@close="noop"
|
||||||
|
@cancel="noop"
|
||||||
|
@confirm="confirmGenderPicker"
|
||||||
|
></uv-picker>
|
||||||
|
|
||||||
|
<uv-picker
|
||||||
|
ref="selectPicker"
|
||||||
|
:title="selectPickerTitle"
|
||||||
|
:columns="selectPickerColumns"
|
||||||
|
:defaultIndex="[selectPickerIndex]"
|
||||||
|
keyName="text"
|
||||||
|
@close="noop"
|
||||||
|
@cancel="noop"
|
||||||
|
@confirm="confirmSelectPicker"
|
||||||
|
></uv-picker>
|
||||||
|
|
||||||
|
<uv-picker
|
||||||
|
ref="datePicker"
|
||||||
|
:title="datePickerTitle"
|
||||||
|
:columns="datePickerColumns"
|
||||||
|
:defaultIndex="datePickerIndexs"
|
||||||
|
keyName="text"
|
||||||
|
@change="handleDatePickerChange"
|
||||||
|
@cancel="noop"
|
||||||
|
@confirm="confirmDatePicker"
|
||||||
|
></uv-picker>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CommonHero from '../../components/common-hero/common-hero.vue'
|
||||||
|
import { createInitialFormData, getFormConfig, getModuleMeta } from '../../utils/gxmu/config'
|
||||||
|
import {
|
||||||
|
addSjqnForm,
|
||||||
|
addSjtbzbsjForm,
|
||||||
|
addWshqtwForm,
|
||||||
|
addWshqtzbForm,
|
||||||
|
addYxgqtdgbForm,
|
||||||
|
addYxgqtyForm,
|
||||||
|
getSjqnForm,
|
||||||
|
getSjtbzbsjForm,
|
||||||
|
getWshqtwForm,
|
||||||
|
getWshqtzbForm,
|
||||||
|
getYxgqtdgbForm,
|
||||||
|
getYxgqtyForm,
|
||||||
|
listDictData,
|
||||||
|
updateSjqnForm,
|
||||||
|
updateSjtbzbsjForm,
|
||||||
|
updateWshqtwForm,
|
||||||
|
updateWshqtzbForm,
|
||||||
|
updateYxgqtdgbForm,
|
||||||
|
updateYxgqtyForm,
|
||||||
|
userPageSjqnForm,
|
||||||
|
userPageSjtbzbsjForm,
|
||||||
|
userPageWshqtwForm,
|
||||||
|
userPageWshqtzbForm,
|
||||||
|
userPageYxgqtdgbForm,
|
||||||
|
userPageYxgqtyForm
|
||||||
|
} from './module-form-service'
|
||||||
|
|
||||||
|
const STORAGE_PREFIX = 'gxmu_form_draft_'
|
||||||
|
const POLITICS_DICT_ID = 172
|
||||||
|
const NATION_DICT_ID = 173
|
||||||
|
const MODULE_API_MAP = {
|
||||||
|
gxmu_sjqn: {
|
||||||
|
get: getSjqnForm,
|
||||||
|
add: addSjqnForm,
|
||||||
|
update: updateSjqnForm,
|
||||||
|
page: userPageSjqnForm
|
||||||
|
},
|
||||||
|
gxmu_sjtbzbsj: {
|
||||||
|
get: getSjtbzbsjForm,
|
||||||
|
add: addSjtbzbsjForm,
|
||||||
|
update: updateSjtbzbsjForm,
|
||||||
|
page: userPageSjtbzbsjForm
|
||||||
|
},
|
||||||
|
gxmu_wshqtw: {
|
||||||
|
get: getWshqtwForm,
|
||||||
|
add: addWshqtwForm,
|
||||||
|
update: updateWshqtwForm,
|
||||||
|
page: userPageWshqtwForm
|
||||||
|
},
|
||||||
|
gxmu_wshqtzb: {
|
||||||
|
get: getWshqtzbForm,
|
||||||
|
add: addWshqtzbForm,
|
||||||
|
update: updateWshqtzbForm,
|
||||||
|
page: userPageWshqtzbForm
|
||||||
|
},
|
||||||
|
gxmu_yxgqtdgb: {
|
||||||
|
get: getYxgqtdgbForm,
|
||||||
|
add: addYxgqtdgbForm,
|
||||||
|
update: updateYxgqtdgbForm,
|
||||||
|
page: userPageYxgqtdgbForm
|
||||||
|
},
|
||||||
|
gxmu_yxgqty: {
|
||||||
|
get: getYxgqtyForm,
|
||||||
|
add: addYxgqtyForm,
|
||||||
|
update: updateYxgqtyForm,
|
||||||
|
page: userPageYxgqtyForm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CommonHero
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
moduleCode: '',
|
||||||
|
moduleMeta: null,
|
||||||
|
formConfig: null,
|
||||||
|
formData: {},
|
||||||
|
draftSavedAt: '',
|
||||||
|
fixedYear: '',
|
||||||
|
declareTitle: '',
|
||||||
|
nationOptions: [],
|
||||||
|
politicsOptions: [],
|
||||||
|
recordId: '',
|
||||||
|
loading: false,
|
||||||
|
showEdit: false,
|
||||||
|
list: [],
|
||||||
|
genderFieldKey: '',
|
||||||
|
genderPickerColumns: [[]],
|
||||||
|
selectFieldKey: '',
|
||||||
|
selectPickerTitle: '',
|
||||||
|
selectPickerColumns: [[]],
|
||||||
|
dateFieldKey: '',
|
||||||
|
datePickerTitle: '',
|
||||||
|
datePickerColumns: [[]],
|
||||||
|
datePickerIndexs: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
pageTitle() {
|
||||||
|
return this.declareTitle || (this.formConfig ? this.formConfig.title : '申报表')
|
||||||
|
},
|
||||||
|
nationLabels() {
|
||||||
|
return this.nationOptions
|
||||||
|
},
|
||||||
|
genderPickerIndex() {
|
||||||
|
const options = (this.genderPickerColumns[0] || []).map((item) => item.value)
|
||||||
|
const index = options.indexOf(this.formData[this.genderFieldKey])
|
||||||
|
return index > -1 ? index : 0
|
||||||
|
},
|
||||||
|
selectPickerIndex() {
|
||||||
|
const options = (this.selectPickerColumns[0] || []).map((item) => item.value)
|
||||||
|
const index = options.indexOf(this.formData[this.selectFieldKey])
|
||||||
|
return index > -1 ? index : 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad(options) {
|
||||||
|
const code = options.code || ''
|
||||||
|
const config = getFormConfig(code)
|
||||||
|
const meta = getModuleMeta(code)
|
||||||
|
const fixedYear = String(options.year || '').trim()
|
||||||
|
const declareTitle = decodeURIComponent(options.declareTitle || '').trim()
|
||||||
|
const recordId = String(options.id || '').trim()
|
||||||
|
console.log('this.moduleMeta', meta)
|
||||||
|
this.moduleCode = code
|
||||||
|
this.formConfig = config || null
|
||||||
|
this.moduleMeta = meta || null
|
||||||
|
this.formData = createInitialFormData(code)
|
||||||
|
this.fixedYear = fixedYear
|
||||||
|
this.declareTitle = declareTitle
|
||||||
|
this.recordId = recordId
|
||||||
|
|
||||||
|
if (config) {
|
||||||
|
uni.setNavigationBarTitle({
|
||||||
|
title: declareTitle || (meta ? meta.title : '申报表')
|
||||||
|
})
|
||||||
|
this.loadDictOptions()
|
||||||
|
this.setupPickerState()
|
||||||
|
this.loadData()
|
||||||
|
this.loadInitialDetail()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getApiConfig() {
|
||||||
|
return MODULE_API_MAP[this.moduleCode] || null
|
||||||
|
},
|
||||||
|
createFreshFormData() {
|
||||||
|
const data = createInitialFormData(this.moduleCode)
|
||||||
|
data.year = this.fixedYear || data.year || String(new Date().getFullYear())
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
async loadInitialDetail() {
|
||||||
|
if (!this.recordId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.loadRemoteRecord(this.recordId)
|
||||||
|
this.showEdit = true
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({
|
||||||
|
title: (error && error.message) || '表单详情加载失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadRemoteRecord(id) {
|
||||||
|
const api = this.getApiConfig()
|
||||||
|
if (!api || !api.get || !id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const detail = await api.get(id)
|
||||||
|
if (!detail) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.formData = {
|
||||||
|
...this.createFreshFormData(),
|
||||||
|
...detail
|
||||||
|
}
|
||||||
|
if (this.fixedYear) {
|
||||||
|
this.formData.year = this.fixedYear
|
||||||
|
}
|
||||||
|
this.setupPickerState()
|
||||||
|
},
|
||||||
|
async loadData() {
|
||||||
|
const api = this.getApiConfig()
|
||||||
|
if (!api || !api.page) {
|
||||||
|
this.list = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const result = await api.page({
|
||||||
|
page: 1,
|
||||||
|
limit: 50,
|
||||||
|
year: this.fixedYear || undefined
|
||||||
|
})
|
||||||
|
this.list = Array.isArray(result && result.list) ? result.list : []
|
||||||
|
} catch (error) {
|
||||||
|
this.list = []
|
||||||
|
uni.showToast({
|
||||||
|
title: (error && error.message) || '加载失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openCreate() {
|
||||||
|
this.formData = this.createFreshFormData()
|
||||||
|
this.draftSavedAt = ''
|
||||||
|
this.restoreDraft()
|
||||||
|
this.setupPickerState()
|
||||||
|
this.showEdit = true
|
||||||
|
},
|
||||||
|
async openEdit(item) {
|
||||||
|
try {
|
||||||
|
const id = item && item.id ? item.id : ''
|
||||||
|
if (id) {
|
||||||
|
await this.loadRemoteRecord(id)
|
||||||
|
} else {
|
||||||
|
this.formData = {
|
||||||
|
...this.createFreshFormData(),
|
||||||
|
...(item || {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.showEdit = true
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({
|
||||||
|
title: (error && error.message) || '加载详情失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeEdit() {
|
||||||
|
this.showEdit = false
|
||||||
|
},
|
||||||
|
getRecordTitle(item) {
|
||||||
|
if (!item) {
|
||||||
|
return this.pageTitle
|
||||||
|
}
|
||||||
|
const fields = ['name', 'title', 'orgName', 'branchName', 'applyType']
|
||||||
|
for (const key of fields) {
|
||||||
|
const value = String(item[key] || '').trim()
|
||||||
|
if (value) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.pageTitle
|
||||||
|
},
|
||||||
|
getRecordSubtitle(item) {
|
||||||
|
const fields = ['applyType', 'unit', 'collegeClass', 'organization', 'secondOrg', 'branch', 'position']
|
||||||
|
const parts = fields
|
||||||
|
.map((key) => String((item && item[key]) || '').trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 1)
|
||||||
|
return parts.length ? parts.join(' · ') : (this.moduleMeta && this.moduleMeta.group) || '申报记录'
|
||||||
|
},
|
||||||
|
getRecordMeta(item) {
|
||||||
|
const pairs = [
|
||||||
|
['政治面貌', item && item.politics],
|
||||||
|
['联系方式', item && (item.contact || item.phone)],
|
||||||
|
['民族', item && item.nation]
|
||||||
|
]
|
||||||
|
const matched = pairs.find((entry) => String(entry[1] || '').trim())
|
||||||
|
if (matched) {
|
||||||
|
return `${matched[0]}:${matched[1]}`
|
||||||
|
}
|
||||||
|
return `记录编号:${(item && item.id) || '-'}`
|
||||||
|
},
|
||||||
|
noop() {
|
||||||
|
return
|
||||||
|
},
|
||||||
|
setupPickerState() {
|
||||||
|
this.genderPickerColumns = [[
|
||||||
|
{ text: '男', value: '男' },
|
||||||
|
{ text: '女', value: '女' }
|
||||||
|
]]
|
||||||
|
},
|
||||||
|
async loadDictOptions() {
|
||||||
|
try {
|
||||||
|
const [nationResult, politicsResult] = await Promise.all([
|
||||||
|
listDictData({ dictId: NATION_DICT_ID }),
|
||||||
|
listDictData({ dictId: POLITICS_DICT_ID })
|
||||||
|
])
|
||||||
|
const normalize = (result) =>
|
||||||
|
(Array.isArray(result) ? result : [])
|
||||||
|
.map((item) => String(item && (item.dictDataName || item.name || item.label || item.value) || '').trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
this.nationOptions = normalize(nationResult)
|
||||||
|
this.politicsOptions = normalize(politicsResult)
|
||||||
|
} catch (error) {
|
||||||
|
this.nationOptions = []
|
||||||
|
this.politicsOptions = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getStorageKey() {
|
||||||
|
return `${STORAGE_PREFIX}${this.moduleCode}_${this.fixedYear || 'default'}`
|
||||||
|
},
|
||||||
|
getLegacyStorageKey() {
|
||||||
|
return `${STORAGE_PREFIX}${this.moduleCode}`
|
||||||
|
},
|
||||||
|
restoreDraft() {
|
||||||
|
const draft =
|
||||||
|
uni.getStorageSync(this.getStorageKey()) ||
|
||||||
|
uni.getStorageSync(this.getLegacyStorageKey())
|
||||||
|
if (!draft || !draft.formData) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.formData = {
|
||||||
|
...this.formData,
|
||||||
|
...draft.formData
|
||||||
|
}
|
||||||
|
if (this.fixedYear) {
|
||||||
|
this.formData.year = this.fixedYear
|
||||||
|
}
|
||||||
|
this.draftSavedAt = draft.savedAt || ''
|
||||||
|
},
|
||||||
|
handleInput(field, event) {
|
||||||
|
const value = typeof event === 'string' ? event : (event.detail && event.detail.value) || ''
|
||||||
|
this.formData[field.key] = value
|
||||||
|
},
|
||||||
|
setFieldValue(key, event) {
|
||||||
|
const value = typeof event === 'string' ? event : (event.detail && event.detail.value) || ''
|
||||||
|
this.formData[key] = value
|
||||||
|
},
|
||||||
|
findFieldByKey(key) {
|
||||||
|
if (!this.formConfig || !Array.isArray(this.formConfig.sections)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
for (const section of this.formConfig.sections) {
|
||||||
|
const matched = (section.fields || []).find((field) => field.key === key)
|
||||||
|
if (matched) {
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
isGenderField(field) {
|
||||||
|
return field && field.key === 'gender'
|
||||||
|
},
|
||||||
|
isPoliticsField(field) {
|
||||||
|
return field && field.key === 'politics'
|
||||||
|
},
|
||||||
|
isNationField(field) {
|
||||||
|
return field && field.key === 'nation'
|
||||||
|
},
|
||||||
|
isUvPickerField(field) {
|
||||||
|
return field && (field.type === 'select' || this.isPoliticsField(field) || this.isNationField(field))
|
||||||
|
},
|
||||||
|
isPickerFieldDisabled(field) {
|
||||||
|
return (this.isPoliticsField(field) || this.isNationField(field)) && !this.getSelectOptions(field).length
|
||||||
|
},
|
||||||
|
getPickerDisplayText(field) {
|
||||||
|
if (this.formData[field.key]) {
|
||||||
|
return this.formData[field.key]
|
||||||
|
}
|
||||||
|
if (this.isPoliticsField(field)) {
|
||||||
|
return this.getSelectOptions(field).length ? '请选择政治面貌' : '政治面貌加载中'
|
||||||
|
}
|
||||||
|
if (this.isNationField(field)) {
|
||||||
|
return this.getSelectOptions(field).length ? '请选择民族' : '民族加载中'
|
||||||
|
}
|
||||||
|
return field.placeholder
|
||||||
|
},
|
||||||
|
getSelectOptions(field) {
|
||||||
|
if (this.isGenderField(field)) {
|
||||||
|
return field.options || ['男', '女']
|
||||||
|
}
|
||||||
|
if (this.isPoliticsField(field)) {
|
||||||
|
return this.politicsOptions.length ? this.politicsOptions : (field.options || [])
|
||||||
|
}
|
||||||
|
return field.options || []
|
||||||
|
},
|
||||||
|
getNationIndex(field) {
|
||||||
|
const index = this.nationOptions.findIndex((item) => item === this.formData[field.key])
|
||||||
|
return index > -1 ? index : 0
|
||||||
|
},
|
||||||
|
handleNationPickerChange(field, event) {
|
||||||
|
const index = Number((event && event.detail && event.detail.value) || 0)
|
||||||
|
this.formData[field.key] = this.nationOptions[index] || ''
|
||||||
|
},
|
||||||
|
openSelectPicker(field) {
|
||||||
|
const options = this.getSelectOptions(field)
|
||||||
|
if (!options.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.selectFieldKey = field.key
|
||||||
|
this.selectPickerTitle = `选择${field.label}`
|
||||||
|
this.selectPickerColumns = [
|
||||||
|
options.map((item) => ({ text: item, value: item }))
|
||||||
|
]
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.selectPicker && this.$refs.selectPicker.open()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
confirmSelectPicker(event) {
|
||||||
|
const value = (((event || {}).value || [])[0] || {}).value || ''
|
||||||
|
if (this.selectFieldKey) {
|
||||||
|
this.formData[this.selectFieldKey] = value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openGenderPicker(field) {
|
||||||
|
this.genderFieldKey = field.key
|
||||||
|
this.genderPickerColumns = [
|
||||||
|
this.getSelectOptions(field).map((item) => ({ text: item, value: item }))
|
||||||
|
]
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.genderPicker && this.$refs.genderPicker.open()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
confirmGenderPicker(event) {
|
||||||
|
const value = (((event || {}).value || [])[0] || {}).value || ''
|
||||||
|
if (this.genderFieldKey) {
|
||||||
|
this.formData[this.genderFieldKey] = value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isDateField(field) {
|
||||||
|
return ['year', 'month', 'date'].includes(field.type)
|
||||||
|
},
|
||||||
|
getDateRange(field) {
|
||||||
|
const start = String(field.start || '2000-01-01').trim()
|
||||||
|
const end = String(field.end || '2099-12-31').trim()
|
||||||
|
return {
|
||||||
|
startYear: Number(start.slice(0, 4)) || 2000,
|
||||||
|
endYear: Number(end.slice(0, 4)) || 2099
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getDaysInMonth(year, month) {
|
||||||
|
return new Date(year, month, 0).getDate()
|
||||||
|
},
|
||||||
|
buildDatePickerState(field, value, nextIndexs = []) {
|
||||||
|
const { startYear, endYear } = this.getDateRange(field)
|
||||||
|
const years = []
|
||||||
|
for (let year = startYear; year <= endYear; year += 1) {
|
||||||
|
years.push({ text: `${year}`, value: `${year}` })
|
||||||
|
}
|
||||||
|
const months = Array.from({ length: 12 }, (_, index) => {
|
||||||
|
const month = String(index + 1).padStart(2, '0')
|
||||||
|
return { text: month, value: month }
|
||||||
|
})
|
||||||
|
const safeValue = String(value || '').trim()
|
||||||
|
let selectedYear = safeValue.slice(0, 4) || `${startYear}`
|
||||||
|
let selectedMonth = safeValue.slice(5, 7) || '01'
|
||||||
|
let selectedDay = safeValue.slice(8, 10) || '01'
|
||||||
|
if (Array.isArray(nextIndexs) && nextIndexs.length) {
|
||||||
|
if (typeof nextIndexs[0] === 'number' && years[nextIndexs[0]]) {
|
||||||
|
selectedYear = years[nextIndexs[0]].value
|
||||||
|
}
|
||||||
|
if (typeof nextIndexs[1] === 'number' && months[nextIndexs[1]]) {
|
||||||
|
selectedMonth = months[nextIndexs[1]].value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const dayTotal = this.getDaysInMonth(Number(selectedYear), Number(selectedMonth))
|
||||||
|
const days = Array.from({ length: dayTotal }, (_, index) => {
|
||||||
|
const day = String(index + 1).padStart(2, '0')
|
||||||
|
return { text: day, value: day }
|
||||||
|
})
|
||||||
|
if (Array.isArray(nextIndexs) && nextIndexs.length > 2 && typeof nextIndexs[2] === 'number' && days[nextIndexs[2]]) {
|
||||||
|
selectedDay = days[nextIndexs[2]].value
|
||||||
|
}
|
||||||
|
const yearIndex = Math.max(0, years.findIndex((item) => item.value === selectedYear))
|
||||||
|
const monthIndex = Math.max(0, months.findIndex((item) => item.value === selectedMonth))
|
||||||
|
const dayIndex = Math.max(0, days.findIndex((item) => item.value === selectedDay))
|
||||||
|
if (field.type === 'year') {
|
||||||
|
return {
|
||||||
|
columns: [years],
|
||||||
|
indexs: [yearIndex]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (field.type === 'month') {
|
||||||
|
return {
|
||||||
|
columns: [years, months],
|
||||||
|
indexs: [yearIndex, monthIndex]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
columns: [years, months, days],
|
||||||
|
indexs: [yearIndex, monthIndex, dayIndex]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openDatePicker(field) {
|
||||||
|
this.dateFieldKey = field.key
|
||||||
|
this.datePickerTitle = `选择${field.label}`
|
||||||
|
const pickerState = this.buildDatePickerState(field, this.formData[field.key])
|
||||||
|
this.datePickerColumns = pickerState.columns
|
||||||
|
this.datePickerIndexs = pickerState.indexs
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.datePicker && this.$refs.datePicker.open()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleDatePickerChange(event) {
|
||||||
|
const field = this.findFieldByKey(this.dateFieldKey)
|
||||||
|
if (!field || field.type !== 'date') {
|
||||||
|
this.datePickerIndexs = (event && event.indexs) || this.datePickerIndexs
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const nextIndexs = (event && event.indexs) || []
|
||||||
|
const pickerState = this.buildDatePickerState(field, this.formData[field.key], nextIndexs)
|
||||||
|
this.datePickerColumns = pickerState.columns
|
||||||
|
this.datePickerIndexs = pickerState.indexs
|
||||||
|
},
|
||||||
|
formatDatePickerValue(values, field) {
|
||||||
|
const year = (values[0] && values[0].value) || ''
|
||||||
|
if (field.type === 'year') {
|
||||||
|
return year
|
||||||
|
}
|
||||||
|
const month = (values[1] && values[1].value) || '01'
|
||||||
|
if (field.type === 'month') {
|
||||||
|
return `${year}-${month}`
|
||||||
|
}
|
||||||
|
const day = (values[2] && values[2].value) || '01'
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
},
|
||||||
|
confirmDatePicker(event) {
|
||||||
|
const field = this.findFieldByKey(this.dateFieldKey)
|
||||||
|
if (!field) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.formData[field.key] = this.formatDatePickerValue((event && event.value) || [], field)
|
||||||
|
this.datePickerIndexs = (event && event.indexs) || this.datePickerIndexs
|
||||||
|
},
|
||||||
|
getDatePlaceholder(field) {
|
||||||
|
if (field.type === 'year') {
|
||||||
|
return '请选择年份'
|
||||||
|
}
|
||||||
|
if (field.type === 'month') {
|
||||||
|
return '请选择年月'
|
||||||
|
}
|
||||||
|
return '请选择日期'
|
||||||
|
},
|
||||||
|
getInputType(field) {
|
||||||
|
if (field.type === 'number' || field.type === 'phone') {
|
||||||
|
return 'number'
|
||||||
|
}
|
||||||
|
return 'text'
|
||||||
|
},
|
||||||
|
validateForm() {
|
||||||
|
const requiredFields = []
|
||||||
|
this.formConfig.sections.forEach((section) => {
|
||||||
|
section.fields.forEach((field) => {
|
||||||
|
if (field.required && !String(this.formData[field.key] || '').trim()) {
|
||||||
|
requiredFields.push(field.label)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!requiredFields.length) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: `请完善:${requiredFields[0]}`,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
saveDraft() {
|
||||||
|
const savedAt = this.formatNow()
|
||||||
|
uni.setStorageSync(this.getStorageKey(), {
|
||||||
|
moduleCode: this.moduleCode,
|
||||||
|
formData: this.formData,
|
||||||
|
savedAt
|
||||||
|
})
|
||||||
|
this.draftSavedAt = savedAt
|
||||||
|
uni.showToast({
|
||||||
|
title: '草稿已保存',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
clearDraftStorage() {
|
||||||
|
uni.removeStorageSync(this.getStorageKey())
|
||||||
|
uni.removeStorageSync(this.getLegacyStorageKey())
|
||||||
|
this.draftSavedAt = ''
|
||||||
|
},
|
||||||
|
async submitForm() {
|
||||||
|
if (!this.validateForm()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const api = this.getApiConfig()
|
||||||
|
if (!api) {
|
||||||
|
this.clearDraftStorage()
|
||||||
|
uni.showModal({
|
||||||
|
title: '提交成功',
|
||||||
|
content: `${this.moduleMeta ? this.moduleMeta.title : '当前模块'}表单已提交,后续可继续接入后台接口。`,
|
||||||
|
showCancel: false
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
...this.formData,
|
||||||
|
year: this.fixedYear || this.formData.year
|
||||||
|
}
|
||||||
|
const request = payload.id ? api.update : api.add
|
||||||
|
await request({
|
||||||
|
...payload,
|
||||||
|
id: payload.id || undefined
|
||||||
|
})
|
||||||
|
this.clearDraftStorage()
|
||||||
|
uni.showToast({
|
||||||
|
title: '提交成功',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
this.closeEdit()
|
||||||
|
this.loadData()
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({
|
||||||
|
title: (error && error.message) || '提交失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatNow() {
|
||||||
|
const now = new Date()
|
||||||
|
const pad = (value) => String(value).padStart(2, '0')
|
||||||
|
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page { min-height: 100vh; background: linear-gradient(180deg, #f5fbff 0%, #eef7ff 100%); }
|
||||||
|
.page-scroll { height: 100vh; box-sizing: border-box; }
|
||||||
|
.hero-wrap{ padding:24rpx 24rpx 0; background: url("@/static/indexBg.jpg") no-repeat cover}
|
||||||
|
.hero-card,.state-card,.data-card { border-radius: 28rpx; }
|
||||||
|
.hero-card {
|
||||||
|
padding: 34rpx 36rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
box-shadow: 0 4rpx 12rpx 0 rgba(174, 174, 174, 0.4);
|
||||||
|
backdrop-filter: blur(24rpx);
|
||||||
|
-webkit-backdrop-filter: blur(24rpx);
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
.hero-badge { display: inline-flex; padding: 10rpx 18rpx; border-radius: 999rpx; background: rgba(20,150,242,.1); font-size: 22rpx; color: #1496f2; }
|
||||||
|
.hero-title {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
padding-left: 10rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: #18a0f7;
|
||||||
|
border-left: 5px solid #0f94ef;
|
||||||
|
}
|
||||||
|
//.hero-title::before {
|
||||||
|
// content: '';
|
||||||
|
// position: absolute;
|
||||||
|
// left: 0;
|
||||||
|
// top: 8rpx;
|
||||||
|
// width: 8rpx;
|
||||||
|
// height: 52rpx;
|
||||||
|
// border-radius: 999rpx;
|
||||||
|
// background: linear-gradient(180deg, #0f94ef 0%, #22b4ff 100%);
|
||||||
|
//}
|
||||||
|
.hero-desc { margin-top: 20rpx; font-size: 24rpx; line-height: 1.8; color: #7f7f7f; }
|
||||||
|
.hero-actions { display: flex; gap: 16rpx; margin-top: 24rpx; }
|
||||||
|
.hero-action { padding: 14rpx 24rpx; border-radius: 999rpx; background: rgba(20,150,242,.12); font-size: 24rpx; color: #1496f2; }
|
||||||
|
.hero-action--secondary { background: rgba(31,35,41,.08); color: #5f6b76; }
|
||||||
|
.state-card,.data-card { margin-top: 24rpx; padding: 24rpx; background: rgba(255,255,255,.96); box-shadow: 0 10rpx 28rpx rgba(34,94,142,.08); }
|
||||||
|
.state-title,.card-title { font-size: 30rpx; font-weight: 700; color: #1f2329; }
|
||||||
|
.state-desc,.card-subtitle,.meta-line { margin-top: 10rpx; font-size: 24rpx; line-height: 1.6; color: #786e69; }
|
||||||
|
.card-list { padding: 0 24rpx; display: flex; flex-direction: column; gap: 16rpx; }
|
||||||
|
.card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 16rpx; }
|
||||||
|
.card-year { padding: 10rpx 16rpx; border-radius: 999rpx; background: #eef5ff; color: #1554ad; font-size: 22rpx; }
|
||||||
|
.modal-mask { position: fixed; inset: 0; background: rgba(15,23,42,.34); display: flex; align-items: flex-end; z-index: 99; }
|
||||||
|
.modal-panel { width: 100%; max-height: 92vh; border-radius: 32rpx 32rpx 0 0; background: #fff; display: flex; flex-direction: column; }
|
||||||
|
.modal-head { display: flex; justify-content: space-between; align-items: center; padding: 24rpx; border-bottom: 1rpx solid #eef1f4; }
|
||||||
|
.modal-title { font-size: 30rpx; font-weight: 700; color: #1f2329; }
|
||||||
|
.modal-close { font-size: 24rpx; color: #7c8a96; }
|
||||||
|
.modal-body { max-height: 70vh; padding: 24rpx; box-sizing: border-box; }
|
||||||
|
.hero-meta--form { grid-template-columns: repeat(2, minmax(0, 1fr)); margin-bottom: 20rpx; }
|
||||||
|
.meta-item--form { background: #f3f7fd; }
|
||||||
|
.meta-label--form { color: #6b7785; }
|
||||||
|
.meta-value--form { color: #1f2329; }
|
||||||
|
.section-card { margin-top: 24rpx; padding: 26rpx; border-radius: 30rpx; background: rgba(255,255,255,.94); border: 1rpx solid rgba(20,150,242,.08); box-shadow: 0 10rpx 28rpx rgba(34,94,142,.08); }
|
||||||
|
.section-head { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 18rpx; }
|
||||||
|
.section-title { font-size: 30rpx; font-weight: 700; color: #1f2329; }
|
||||||
|
.field-list { display: flex; flex-direction: column; gap: 22rpx; }
|
||||||
|
.field-label { margin-bottom: 12rpx; font-size: 25rpx; font-weight: 600; color: #2b3037; }
|
||||||
|
.field-required { margin-left: 8rpx; color: #1496f2; }
|
||||||
|
.field-textarea,.field-picker { width: 100%; box-sizing: border-box; padding: 22rpx 24rpx; border-radius: 22rpx; font-size: 24rpx; color: #2a2e35; }
|
||||||
|
.field-textarea { background: #f8f4f1; border: 1rpx solid #eadfd7; }
|
||||||
|
.field-picker { background: #F4FCFF; border: 1rpx solid rgba(220,220,220,1); }
|
||||||
|
.field-picker--disabled { background: #f7f9fb; color: #aaafb7; }
|
||||||
|
.field-textarea { min-height: 180rpx; }
|
||||||
|
:deep(.field-input.uv-input) { width: 100%; min-height: 88rpx; padding: 0 24rpx; border-radius: 12rpx; border: 1px solid #dcdfe6; background: #ffffff; box-sizing: border-box; transition: border-color 0.2s ease; }
|
||||||
|
:deep(.field-input.uv-input .uv-input__content__field-wrapper__field) { height: 88rpx; min-height: 88rpx; font-size: 28rpx; line-height: 88rpx; color: #303133; }
|
||||||
|
.field-placeholder { color: #aa9f98; }
|
||||||
|
.field-value { color: #2a2e35; }
|
||||||
|
.modal-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16rpx; padding: 24rpx; border-top: 1rpx solid #eef1f4; }
|
||||||
|
.ghost-btn,.primary-btn { height: 84rpx; line-height: 84rpx; border-radius: 22rpx; text-align: center; font-size: 26rpx; font-weight: 700; }
|
||||||
|
.ghost-btn { background: #FFA2DA; color: #ffffff; border: none; border-radius: 999px; }
|
||||||
|
.primary-btn { background: linear-gradient(147.22deg, rgba(0,180,255,1) 37.65%, rgba(88,206,255,1) 80.42%); color: #fff; border-radius: 999px; }
|
||||||
|
.empty-page { min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48rpx; text-align: center; }
|
||||||
|
.empty-title { font-size: 30rpx; font-weight: 700; color: #1f2329; }
|
||||||
|
.empty-desc { margin-top: 12rpx; font-size: 24rpx; line-height: 1.7; color: #7b8694; }
|
||||||
|
</style>
|
||||||
697
pages/gxmu/index.vue
Normal file
@@ -0,0 +1,697 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<scroll-view scroll-y class="page-scroll">
|
||||||
|
<common-hero title="五四评优" :use-image-bg="true"></common-hero>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-head">
|
||||||
|
<view>
|
||||||
|
<view class="section-title">项目列表</view>
|
||||||
|
<view class="section-subtitle">当前仅展示可申报项目</view>
|
||||||
|
</view>
|
||||||
|
<view class="refresh-btn" :class="{ 'refresh-btn--loading': loading }" @tap="loadData()">
|
||||||
|
{{ loading ? '刷新中' : '刷新' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading" class="state-card">
|
||||||
|
<view class="state-title">正在加载申报项目...</view>
|
||||||
|
<view class="state-desc">请稍候,正在同步后台开放的申报配置。</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else-if="loadError" class="state-card state-card--error">
|
||||||
|
<view class="state-title">加载失败</view>
|
||||||
|
<view class="state-desc">{{ loadError }}</view>
|
||||||
|
<view class="state-action" @tap="loadData()">重新加载</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else-if="!declareList.length" class="state-card">
|
||||||
|
<view class="state-title">当前暂无可申报项目</view>
|
||||||
|
<view class="state-desc">后台还没有开放可申报数据,稍后再来查看。</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else>
|
||||||
|
<view class="filter-bar">
|
||||||
|
<picker class="filter-item" mode="selector" :range="yearLabels" :value="yearIndex" @change="handleYearChange">
|
||||||
|
<view class="filter-trigger">
|
||||||
|
<text class="filter-text">{{ selectedYear ? `${selectedYear} 年` : '全部年份' }}</text>
|
||||||
|
<text class="filter-arrow">▼</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
<view class="filter-item" @tap="openModulePicker">
|
||||||
|
<view class="filter-trigger">
|
||||||
|
<text class="filter-text">{{ selectedModule ? getDeclareModuleLabel(selectedModule) : '全部模块' }}</text>
|
||||||
|
<text class="filter-arrow">▼</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="filteredDeclareList.length" class="declare-list">
|
||||||
|
<view
|
||||||
|
v-for="item in filteredDeclareList"
|
||||||
|
:key="item.id || `${item.module}-${item.year}`"
|
||||||
|
class="declare-card"
|
||||||
|
:class="{ 'declare-card--disabled': !isModuleSupported(item.module) }"
|
||||||
|
@tap="goApply(item)"
|
||||||
|
>
|
||||||
|
<view class="card-top">
|
||||||
|
<view class="card-main">
|
||||||
|
<view class="card-title">{{ getDeclareTitle(item) }}</view>
|
||||||
|
<view class="card-subtitle">{{ getDeclareModuleLabel(item.module) }}</view>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="card-status"
|
||||||
|
:class="isModuleSupported(item.module) ? 'card-status--active' : 'card-status--disabled'"
|
||||||
|
>
|
||||||
|
{{ isModuleSupported(item.module) ? '可申报' : '待支持' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-year">
|
||||||
|
{{ item.year ? `${item.year} 年` : '未设置年度' }}
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-desc">{{ getDeclareDescription(item.module) }}</view>
|
||||||
|
|
||||||
|
<view class="card-meta">
|
||||||
|
<view class="meta-row">
|
||||||
|
<view class="meta-row-label">开始时间</view>
|
||||||
|
<view class="meta-row-value">{{ formatDateTime(item.startTime) }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-row">
|
||||||
|
<view class="meta-row-label">结束时间</view>
|
||||||
|
<view class="meta-row-value">{{ formatDateTime(item.endTime) }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-footer">
|
||||||
|
<view class="card-tag">{{ getDeclareGroup(item.module) }}</view>
|
||||||
|
<view class="card-action">
|
||||||
|
{{ isModuleSupported(item.module) ? '进入申报' : '暂未开放' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="state-card">
|
||||||
|
<view class="state-title">暂无匹配项目</view>
|
||||||
|
<view class="state-desc">请调整年份或模块筛选条件后重试。</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<uv-picker
|
||||||
|
ref="modulePicker"
|
||||||
|
title="选择模块"
|
||||||
|
:columns="modulePickerColumns"
|
||||||
|
:defaultIndex="[moduleIndex]"
|
||||||
|
keyName="text"
|
||||||
|
@close="noop"
|
||||||
|
@cancel="noop"
|
||||||
|
@confirm="confirmModulePicker"
|
||||||
|
></uv-picker>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CommonHero from '../../components/common-hero/common-hero.vue'
|
||||||
|
import { getFormConfig, getModuleMeta } from '../../utils/gxmu/config'
|
||||||
|
import { listDeclare } from '../../utils/gxmu/declare-service'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CommonHero
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
loadError: '',
|
||||||
|
declareList: [],
|
||||||
|
selectedYear: '',
|
||||||
|
selectedModule: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
supportedCount() {
|
||||||
|
return this.declareList.filter((item) => this.isModuleSupported(item.module)).length
|
||||||
|
},
|
||||||
|
latestYear() {
|
||||||
|
const year = this.declareList.reduce((result, item) => {
|
||||||
|
const current = Number(item.year || 0)
|
||||||
|
return current > result ? current : result
|
||||||
|
}, 0)
|
||||||
|
return year || new Date().getFullYear()
|
||||||
|
},
|
||||||
|
yearOptions() {
|
||||||
|
const set = new Set(
|
||||||
|
this.declareList
|
||||||
|
.map((item) => String(item.year || '').trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
)
|
||||||
|
return [...set].sort((left, right) => Number(right) - Number(left))
|
||||||
|
},
|
||||||
|
moduleOptions() {
|
||||||
|
const set = new Set(
|
||||||
|
this.declareList
|
||||||
|
.map((item) => String(item.module || '').trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
)
|
||||||
|
return [...set]
|
||||||
|
},
|
||||||
|
yearLabels() {
|
||||||
|
return ['全部年份', ...this.yearOptions.map((item) => `${item} 年`)]
|
||||||
|
},
|
||||||
|
moduleLabels() {
|
||||||
|
return ['全部模块', ...this.moduleOptions.map((item) => this.getDeclareModuleLabel(item))]
|
||||||
|
},
|
||||||
|
modulePickerColumns() {
|
||||||
|
return [[
|
||||||
|
{ text: '全部模块', value: '' },
|
||||||
|
...this.moduleOptions.map((item) => ({
|
||||||
|
text: this.getDeclareModuleLabel(item),
|
||||||
|
value: item
|
||||||
|
}))
|
||||||
|
]]
|
||||||
|
},
|
||||||
|
yearIndex() {
|
||||||
|
if (!this.selectedYear) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const index = this.yearOptions.findIndex((item) => item === this.selectedYear)
|
||||||
|
return index > -1 ? index + 1 : 0
|
||||||
|
},
|
||||||
|
moduleIndex() {
|
||||||
|
if (!this.selectedModule) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const index = this.moduleOptions.findIndex((item) => item === this.selectedModule)
|
||||||
|
return index > -1 ? index + 1 : 0
|
||||||
|
},
|
||||||
|
filteredDeclareList() {
|
||||||
|
return this.declareList.filter((item) => {
|
||||||
|
const yearMatched = !this.selectedYear || String(item.year || '').trim() === this.selectedYear
|
||||||
|
const moduleMatched = !this.selectedModule || String(item.module || '').trim() === this.selectedModule
|
||||||
|
return yearMatched && moduleMatched
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
this.loadData()
|
||||||
|
},
|
||||||
|
onPullDownRefresh() {
|
||||||
|
this.loadData(true)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showToast(title) {
|
||||||
|
uni.showToast({
|
||||||
|
title,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getModuleMetaSafe(code) {
|
||||||
|
return getModuleMeta(code) || {
|
||||||
|
title: code || '未命名模块',
|
||||||
|
icon: (code || '申').slice(0, 1),
|
||||||
|
group: '申报项目',
|
||||||
|
desc: '当前申报项目已开放,点击可查看申报内容。'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getDeclareTitle(item) {
|
||||||
|
const title = String(item.title || '').trim()
|
||||||
|
if (title) {
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
return this.getDeclareModuleLabel(item.module)
|
||||||
|
},
|
||||||
|
getDeclareModuleLabel(module) {
|
||||||
|
return this.getModuleMetaSafe(module).title
|
||||||
|
},
|
||||||
|
getDeclareDescription(module) {
|
||||||
|
return this.getModuleMetaSafe(module).desc
|
||||||
|
},
|
||||||
|
getDeclareGroup(module) {
|
||||||
|
return this.getModuleMetaSafe(module).group
|
||||||
|
},
|
||||||
|
isModuleSupported(module) {
|
||||||
|
return !!getFormConfig(module)
|
||||||
|
},
|
||||||
|
getTimestamp(value) {
|
||||||
|
if (!value) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const result = new Date(String(value).replace(/-/g, '/')).getTime()
|
||||||
|
return Number.isNaN(result) ? 0 : result
|
||||||
|
},
|
||||||
|
formatDateTime(value) {
|
||||||
|
if (!value) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
const date = new Date(String(value).replace(/-/g, '/'))
|
||||||
|
const pad = (item) => String(item).padStart(2, '0')
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||||
|
},
|
||||||
|
handleYearChange(event) {
|
||||||
|
const index = Number((event.detail && event.detail.value) || 0)
|
||||||
|
this.selectedYear = index === 0 ? '' : (this.yearOptions[index - 1] || '')
|
||||||
|
},
|
||||||
|
handleModuleChange(event) {
|
||||||
|
const index = Number((event.detail && event.detail.value) || 0)
|
||||||
|
this.selectedModule = index === 0 ? '' : (this.moduleOptions[index - 1] || '')
|
||||||
|
},
|
||||||
|
openModulePicker() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.modulePicker && this.$refs.modulePicker.open()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
confirmModulePicker(event) {
|
||||||
|
const value = (((event || {}).value || [])[0] || {}).value || ''
|
||||||
|
this.selectedModule = value
|
||||||
|
},
|
||||||
|
noop() {
|
||||||
|
return
|
||||||
|
},
|
||||||
|
syncFilterState() {
|
||||||
|
if (this.selectedYear && !this.yearOptions.includes(this.selectedYear)) {
|
||||||
|
this.selectedYear = ''
|
||||||
|
}
|
||||||
|
if (this.selectedModule && !this.moduleOptions.includes(this.selectedModule)) {
|
||||||
|
this.selectedModule = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadData(fromPullDown = false) {
|
||||||
|
if (this.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.loading = true
|
||||||
|
this.loadError = ''
|
||||||
|
try {
|
||||||
|
const result = await listDeclare({ usable: true })
|
||||||
|
const list = Array.isArray(result) ? result : []
|
||||||
|
this.declareList = [...list].sort((left, right) => {
|
||||||
|
const yearDiff = Number(right.year || 0) - Number(left.year || 0)
|
||||||
|
if (yearDiff !== 0) {
|
||||||
|
return yearDiff
|
||||||
|
}
|
||||||
|
return this.getTimestamp(right.startTime || right.createTime) - this.getTimestamp(left.startTime || left.createTime)
|
||||||
|
})
|
||||||
|
this.syncFilterState()
|
||||||
|
} catch (error) {
|
||||||
|
this.loadError = (error && error.message) || '申报项目加载失败'
|
||||||
|
this.showToast(this.loadError)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
if (fromPullDown) {
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
goApply(item) {
|
||||||
|
if (!item.module) {
|
||||||
|
this.showToast('当前申报项目缺少模块标识')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.isModuleSupported(item.module)) {
|
||||||
|
this.showToast('当前申报项目在小程序端暂未配置')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const moduleRouteMap = {
|
||||||
|
gxmu_tzbcy_form: '/pages/gxmu/tzbcy',
|
||||||
|
gxmu_qmgc_form: '/pages/gxmu/qmgc',
|
||||||
|
gxmu_wxxzx_project: '/pages/gxmu/wxxzx'
|
||||||
|
}
|
||||||
|
const moduleRoute = moduleRouteMap[item.module]
|
||||||
|
if (moduleRoute) {
|
||||||
|
const query = [
|
||||||
|
`code=${encodeURIComponent(item.module)}`,
|
||||||
|
`year=${encodeURIComponent(item.year || '')}`,
|
||||||
|
`declareTitle=${encodeURIComponent(this.getDeclareTitle(item))}`,
|
||||||
|
`id=${encodeURIComponent(item.id)}`
|
||||||
|
]
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `${moduleRoute}?${query.join('&')}`
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const query = [
|
||||||
|
`code=${encodeURIComponent(item.module)}`,
|
||||||
|
`year=${encodeURIComponent(item.year || '')}`,
|
||||||
|
`declareTitle=${encodeURIComponent(this.getDeclareTitle(item))}`
|
||||||
|
]
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/gxmu/form?${query.join('&')}`
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goStatsCenter() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/gxmu/stats'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goCrossSchoolBoard() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/gxmu/cross-school-board'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goTzbDatabase() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/gxmu/tzb-database'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(20, 150, 242, 0.14), transparent 30%),
|
||||||
|
linear-gradient(180deg, #f5fbff 0%, #eef7ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-scroll {
|
||||||
|
height: 100vh;
|
||||||
|
padding: 28rpx 24rpx 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
padding: 34rpx 30rpx;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
background: linear-gradient(145deg, #0d6fba 0%, #1496f2 56%, #5bc2ff 100%);
|
||||||
|
box-shadow: 0 18rpx 44rpx rgba(20, 118, 194, 0.2);
|
||||||
|
color: #f7fcff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.16);
|
||||||
|
font-size: 22rpx;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
margin-top: 22rpx;
|
||||||
|
font-size: 42rpx;
|
||||||
|
line-height: 1.35;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-desc {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
font-size: 25rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: rgba(255, 247, 244, 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-meta {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-action {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16rpx 26rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f7fcff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-action--secondary {
|
||||||
|
background: rgba(13, 36, 61, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-action--tertiary {
|
||||||
|
background: rgba(8, 74, 61, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
padding: 18rpx 16rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-label {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: rgba(255, 247, 244, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-top: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20rpx;
|
||||||
|
padding: 0 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-subtitle {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #8b827d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 12rpx 24rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(20, 150, 242, 0.08);
|
||||||
|
color: #1496f2;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn--loading {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
padding: 30rpx 26rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border: 1rpx solid rgba(20, 150, 242, 0.08);
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(34, 94, 142, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card--error {
|
||||||
|
border-color: rgba(20, 150, 242, 0.18);
|
||||||
|
background: rgba(240, 248, 255, 0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-desc {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #786e69;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-action {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1496f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 84rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
border: 1rpx solid rgba(21, 84, 173, 0.08);
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #1f2329;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-arrow {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #7b8ba1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.declare-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.declare-card {
|
||||||
|
padding: 26rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(240, 247, 255, 0.95) 0%, #ffffff 44%),
|
||||||
|
#ffffff;
|
||||||
|
border: 1rpx solid rgba(21, 84, 173, 0.08);
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(76, 49, 35, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.declare-card--disabled {
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 31rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: #1f2329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #5f6b7a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-status {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 10rpx 16rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-status--active {
|
||||||
|
background: #edf5ff;
|
||||||
|
color: #1554ad;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-status--disabled {
|
||||||
|
background: #f5f1ee;
|
||||||
|
color: #8f837d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-year {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #eef5ff;
|
||||||
|
color: #1554ad;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #716863;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-row {
|
||||||
|
padding: 18rpx 20rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: rgba(246, 248, 250, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-row-label {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #8b827d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-row-value {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #1f2329;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-tag {
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #f8f1ea;
|
||||||
|
color: #8a5a22;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-action {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1496f2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
212
pages/gxmu/module-form-service.js
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import { API_BASE_URL } from '../../utils/request'
|
||||||
|
import { apiRequest, getToken } from '../assistant/chat-service'
|
||||||
|
|
||||||
|
function request(url, method = 'GET', data) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${API_BASE_URL}${url}`,
|
||||||
|
method,
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userPageTzbcyForm(params) {
|
||||||
|
return request('/gxmu/tzbcy-form/userPage', 'GET', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTzbcyForm(id) {
|
||||||
|
return request(`/gxmu/tzbcy-form/${id}`, 'GET')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addTzbcyForm(data) {
|
||||||
|
return request('/gxmu/tzbcy-form', 'POST', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTzbcyForm(data) {
|
||||||
|
return request('/gxmu/tzbcy-form', 'PUT', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeTzbcyForm(id) {
|
||||||
|
return request(`/gxmu/tzbcy-form/${id}`, 'DELETE')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userPageSjqnForm(params) {
|
||||||
|
return request('/gxmu/sjqn-form/userPage', 'GET', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSjqnForm(id) {
|
||||||
|
return request(`/gxmu/sjqn-form/${id}`, 'GET')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addSjqnForm(data) {
|
||||||
|
return request('/gxmu/sjqn-form', 'POST', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSjqnForm(data) {
|
||||||
|
return request('/gxmu/sjqn-form', 'PUT', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeSjqnForm(id) {
|
||||||
|
return request(`/gxmu/sjqn-form/${id}`, 'DELETE')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDeclare(id) {
|
||||||
|
return request(`/gxmu/declare/${id}`, 'GET')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listDictData(params) {
|
||||||
|
return request('/system/dict-data', 'GET', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userPageQmgcForm(params) {
|
||||||
|
return request('/gxmu/qmgc-form/userPage', 'GET', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getQmgcForm(id) {
|
||||||
|
return request(`/gxmu/qmgc-form/${id}`, 'GET')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addQmgcForm(data) {
|
||||||
|
return request('/gxmu/qmgc-form', 'POST', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateQmgcForm(data) {
|
||||||
|
return request('/gxmu/qmgc-form', 'PUT', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeQmgcForm(id) {
|
||||||
|
return request(`/gxmu/qmgc-form/${id}`, 'DELETE')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userPageWxxzxForm(params) {
|
||||||
|
return request('/gxmu/wxxzx-form/userPage', 'GET', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWxxzxForm(id) {
|
||||||
|
return request(`/gxmu/wxxzx-form/${id}`, 'GET')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addWxxzxForm(data) {
|
||||||
|
return request('/gxmu/wxxzx-form', 'POST', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateWxxzxForm(data) {
|
||||||
|
return request('/gxmu/wxxzx-form', 'PUT', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeWxxzxForm(id) {
|
||||||
|
return request(`/gxmu/wxxzx-form/${id}`, 'DELETE')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userPageSjtbzbsjForm(params) {
|
||||||
|
return request('/gxmu/sjtbzbsj-form/userPage', 'GET', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSjtbzbsjForm(id) {
|
||||||
|
return request(`/gxmu/sjtbzbsj-form/${id}`, 'GET')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addSjtbzbsjForm(data) {
|
||||||
|
return request('/gxmu/sjtbzbsj-form', 'POST', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSjtbzbsjForm(data) {
|
||||||
|
return request('/gxmu/sjtbzbsj-form', 'PUT', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userPageWshqtwForm(params) {
|
||||||
|
return request('/gxmu/wshqtw-form/userPage', 'GET', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWshqtwForm(id) {
|
||||||
|
return request(`/gxmu/wshqtw-form/${id}`, 'GET')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addWshqtwForm(data) {
|
||||||
|
return request('/gxmu/wshqtw-form', 'POST', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateWshqtwForm(data) {
|
||||||
|
return request('/gxmu/wshqtw-form', 'PUT', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userPageWshqtzbForm(params) {
|
||||||
|
return request('/gxmu/wshqtzb-form/userPage', 'GET', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWshqtzbForm(id) {
|
||||||
|
return request(`/gxmu/wshqtzb-form/${id}`, 'GET')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addWshqtzbForm(data) {
|
||||||
|
return request('/gxmu/wshqtzb-form', 'POST', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateWshqtzbForm(data) {
|
||||||
|
return request('/gxmu/wshqtzb-form', 'PUT', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userPageYxgqtdgbForm(params) {
|
||||||
|
return request('/gxmu/yxgqtdgb-form/userPage', 'GET', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getYxgqtdgbForm(id) {
|
||||||
|
return request(`/gxmu/yxgqtdgb-form/${id}`, 'GET')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addYxgqtdgbForm(data) {
|
||||||
|
return request('/gxmu/yxgqtdgb-form', 'POST', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateYxgqtdgbForm(data) {
|
||||||
|
return request('/gxmu/yxgqtdgb-form', 'PUT', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userPageYxgqtyForm(params) {
|
||||||
|
return request('/gxmu/yxgqty-form/userPage', 'GET', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getYxgqtyForm(id) {
|
||||||
|
return request(`/gxmu/yxgqty-form/${id}`, 'GET')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addYxgqtyForm(data) {
|
||||||
|
return request('/gxmu/yxgqty-form', 'POST', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateYxgqtyForm(data) {
|
||||||
|
return request('/gxmu/yxgqty-form', 'PUT', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadTempFile(file) {
|
||||||
|
const token = getToken()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.uploadFile({
|
||||||
|
url: `${API_BASE_URL}/file/upload`,
|
||||||
|
filePath: file.path,
|
||||||
|
name: 'file',
|
||||||
|
header: token
|
||||||
|
? {
|
||||||
|
Authorization: token
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
formData: {
|
||||||
|
tenantId: '10049'
|
||||||
|
},
|
||||||
|
success: (response) => {
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(response.data || '{}')
|
||||||
|
if (result.code === 0 && result.data) {
|
||||||
|
resolve(result.data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reject(new Error(result.message || '上传失败'))
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error('上传响应解析失败'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
reject(new Error(error.errMsg || '上传失败'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
432
pages/gxmu/qmgc.vue
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<scroll-view scroll-y class="page-scroll">
|
||||||
|
<common-hero :title="pageTitle" :use-image-bg="true"></common-hero>
|
||||||
|
<view class="hero-card">
|
||||||
|
<!-- <view class="hero-badge">GXMU · 青马工程</view>-->
|
||||||
|
<view class="hero-title">{{ pageTitle }}</view>
|
||||||
|
<!-- <view class="hero-desc">按照后台青马工程登记表字段组织,包含基础信息、奖惩情况、培养意向等内容。</view>-->
|
||||||
|
<view class="hero-actions">
|
||||||
|
<view class="hero-action" @tap="openCreate">立即申报</view>
|
||||||
|
<view class="hero-action hero-action--secondary" @tap="loadData">刷新列表</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading" class="state-card"><view class="state-title">正在加载申报记录...</view></view>
|
||||||
|
<view v-else-if="!list.length" class="state-card"><view class="state-title">暂无申报记录</view></view>
|
||||||
|
<view v-else class="card-list">
|
||||||
|
<view v-for="item in list" :key="item.id" class="data-card" @tap="openEdit(item)">
|
||||||
|
<view class="card-top">
|
||||||
|
<view><view class="card-title">{{ item.name || '未命名学员' }}</view><view class="card-subtitle">{{ item.schoolInfo || '-' }}</view></view>
|
||||||
|
<view class="card-year">{{ item.year || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-line">手机号:{{ item.phone || '-' }}</view>
|
||||||
|
<view class="meta-line">团学职务:{{ item.leaguePosition || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view v-if="showEdit" class="modal-mask" @tap="closeEdit">
|
||||||
|
<view class="modal-panel" @tap.stop>
|
||||||
|
<view class="modal-head"><view class="modal-title">{{ current.id ? '编辑青马工程申报' : '新增青马工程申报' }}</view><view class="modal-close" @tap="closeEdit">关闭</view></view>
|
||||||
|
<scroll-view scroll-y class="modal-body">
|
||||||
|
<view class="form-section">
|
||||||
|
<view class="form-section-title">登记表</view>
|
||||||
|
<view class="field-item"><view class="field-label">年份</view><uv-input class="field-input" :modelValue="current.year" border="none" @input="setField('year', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">姓名</view><uv-input class="field-input" :modelValue="current.name" border="none" @input="setField('name', $event)" /></view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">性别</view>
|
||||||
|
<view class="field-picker" @tap="openGenderPicker">
|
||||||
|
{{ current.gender || '请选择性别' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">民族</view>
|
||||||
|
<picker mode="selector" :range="nationLabels" :value="nationIndex" :disabled="!nationOptions.length" @change="handleNationChange">
|
||||||
|
<view class="field-picker" :class="{ 'field-picker--disabled': !nationOptions.length }">
|
||||||
|
{{ current.nation || (nationOptions.length ? '请选择民族' : '民族加载中') }}
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">出生年月</view>
|
||||||
|
<view class="field-picker" @tap="openBirthMonthPicker">
|
||||||
|
{{ current.birthMonth || '请选择出生年月' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">政治面貌</view>
|
||||||
|
<view class="field-picker" :class="{ 'field-picker--disabled': !politicsOptions.length }" @tap="openPoliticsPicker">
|
||||||
|
{{ current.politics || (politicsOptions.length ? '请选择政治面貌' : '政治面貌加载中') }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="field-item"><view class="field-label">籍贯</view><uv-input class="field-input" :modelValue="current.nativePlace" border="none" @input="setField('nativePlace', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">手机号码</view><uv-input class="field-input" :modelValue="current.phone" border="none" @input="setField('phone', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">微信号</view><uv-input class="field-input" :modelValue="current.wechat" border="none" @input="setField('wechat', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">电子邮箱</view><uv-input class="field-input" :modelValue="current.email" border="none" @input="setField('email', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">QQ号</view><uv-input class="field-input" :modelValue="current.qq" border="none" @input="setField('qq', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">身份证号</view><uv-input class="field-input" :modelValue="current.idCardNo" border="none" @input="setField('idCardNo', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">爱好特长</view><uv-input class="field-input" :modelValue="current.hobby" border="none" @input="setField('hobby', $event)" /></view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">是否有志到广西基层工作</view>
|
||||||
|
<view class="field-picker" @tap="openWillingPicker">
|
||||||
|
{{ current.willingToWorkInGuangxi || '请选择' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="field-item"><view class="field-label">学校、院系、年级、专业</view><uv-input class="field-input" :modelValue="current.schoolInfo" border="none" @input="setField('schoolInfo', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">担任团学职务情况</view><uv-input class="field-input" :modelValue="current.leaguePosition" border="none" @input="setField('leaguePosition', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">个人简历</view><textarea class="field-textarea" :value="current.resume" @input="setField('resume', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">奖惩情况</view><textarea class="field-textarea" :value="current.awards" @input="setField('awards', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">综合成绩情况</view><textarea class="field-textarea" :value="current.academicPerformance" @input="setField('academicPerformance', $event)" /></view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="modal-actions"><view class="ghost-btn" @tap="closeEdit">取消</view><view class="primary-btn" @tap="save">保存</view></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<uv-picker
|
||||||
|
ref="willingPicker"
|
||||||
|
keyName="text"
|
||||||
|
:columns="willingPickerColumns"
|
||||||
|
:defaultIndex="[willingPickerIndex]"
|
||||||
|
closeOnClickOverlay
|
||||||
|
@confirm="confirmWillingPicker"
|
||||||
|
></uv-picker>
|
||||||
|
|
||||||
|
<uv-picker
|
||||||
|
ref="genderPicker"
|
||||||
|
title="选择性别"
|
||||||
|
keyName="text"
|
||||||
|
:columns="genderPickerColumns"
|
||||||
|
:defaultIndex="[genderPickerIndex]"
|
||||||
|
closeOnClickOverlay
|
||||||
|
@confirm="confirmGenderPicker"
|
||||||
|
></uv-picker>
|
||||||
|
|
||||||
|
<uv-picker
|
||||||
|
ref="politicsPicker"
|
||||||
|
title="选择政治面貌"
|
||||||
|
keyName="text"
|
||||||
|
:columns="politicsPickerColumns"
|
||||||
|
:defaultIndex="[politicsPickerIndex]"
|
||||||
|
closeOnClickOverlay
|
||||||
|
@confirm="confirmPoliticsPicker"
|
||||||
|
></uv-picker>
|
||||||
|
|
||||||
|
<uv-picker
|
||||||
|
ref="birthMonthPicker"
|
||||||
|
title="选择出生年月"
|
||||||
|
keyName="text"
|
||||||
|
:columns="birthMonthPickerColumns"
|
||||||
|
:defaultIndex="birthMonthPickerIndexs"
|
||||||
|
closeOnClickOverlay
|
||||||
|
@change="handleBirthMonthPickerChange"
|
||||||
|
@confirm="confirmBirthMonthPicker"
|
||||||
|
></uv-picker>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CommonHero from '../../components/common-hero/common-hero.vue'
|
||||||
|
import { addQmgcForm, getQmgcForm, listDictData, updateQmgcForm, userPageQmgcForm } from './module-form-service'
|
||||||
|
|
||||||
|
const POLITICS_DICT_ID = 172
|
||||||
|
const NATION_DICT_ID = 173
|
||||||
|
|
||||||
|
const createDefaultForm = () => ({
|
||||||
|
id: undefined,
|
||||||
|
year: '',
|
||||||
|
name: '',
|
||||||
|
gender: '',
|
||||||
|
nation: '',
|
||||||
|
photo: '',
|
||||||
|
birthMonth: '',
|
||||||
|
politics: '',
|
||||||
|
nativePlace: '',
|
||||||
|
phone: '',
|
||||||
|
wechat: '',
|
||||||
|
email: '',
|
||||||
|
qq: '',
|
||||||
|
idCardNo: '',
|
||||||
|
hobby: '',
|
||||||
|
willingToWorkInGuangxi: '',
|
||||||
|
schoolInfo: '',
|
||||||
|
leaguePosition: '',
|
||||||
|
resume: '',
|
||||||
|
awards: '',
|
||||||
|
academicPerformance: '',
|
||||||
|
secondaryLeagueOpinion: '',
|
||||||
|
secondaryLeagueOpinionDate: '',
|
||||||
|
secondaryPartyOpinion: '',
|
||||||
|
secondaryPartyOpinionDate: '',
|
||||||
|
schoolLeagueOpinion: '',
|
||||||
|
schoolLeagueOpinionDate: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const normalizeDictOptions = (list) =>
|
||||||
|
(Array.isArray(list) ? list : [])
|
||||||
|
.map((item) => {
|
||||||
|
const label = String((item && (item.dictDataName || item.label || item.name || item.value)) || '').trim()
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
value: label
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((item) => item.label)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CommonHero
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
showEdit: false,
|
||||||
|
pageTitle: '青马工程申报',
|
||||||
|
fixedYear: '',
|
||||||
|
list: [],
|
||||||
|
current: createDefaultForm(),
|
||||||
|
nationOptions: [],
|
||||||
|
politicsOptions: [],
|
||||||
|
willingPickerColumns: [[
|
||||||
|
{ text: '是', value: '是' },
|
||||||
|
{ text: '否', value: '否' }
|
||||||
|
]],
|
||||||
|
genderPickerColumns: [[
|
||||||
|
{ text: '男', value: '男' },
|
||||||
|
{ text: '女', value: '女' }
|
||||||
|
]],
|
||||||
|
birthMonthPickerColumns: [[]],
|
||||||
|
birthMonthPickerIndexs: [0, 0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
nationLabels() {
|
||||||
|
return this.nationOptions.map((item) => item.label)
|
||||||
|
},
|
||||||
|
politicsLabels() {
|
||||||
|
return this.politicsOptions.map((item) => item.label)
|
||||||
|
},
|
||||||
|
nationIndex() {
|
||||||
|
const index = this.nationOptions.findIndex((item) => item.value === this.current.nation)
|
||||||
|
return index > -1 ? index : 0
|
||||||
|
},
|
||||||
|
politicsIndex() {
|
||||||
|
const index = this.politicsOptions.findIndex((item) => item.value === this.current.politics)
|
||||||
|
return index > -1 ? index : 0
|
||||||
|
},
|
||||||
|
genderPickerIndex() {
|
||||||
|
const options = (this.genderPickerColumns[0] || []).map((item) => item.value)
|
||||||
|
const index = options.indexOf(this.current.gender)
|
||||||
|
return index > -1 ? index : 0
|
||||||
|
},
|
||||||
|
politicsPickerColumns() {
|
||||||
|
return [
|
||||||
|
this.politicsOptions.map((item) => ({
|
||||||
|
text: item.label,
|
||||||
|
value: item.value
|
||||||
|
}))
|
||||||
|
]
|
||||||
|
},
|
||||||
|
politicsPickerIndex() {
|
||||||
|
const options = (this.politicsPickerColumns[0] || []).map((item) => item.value)
|
||||||
|
const index = options.indexOf(this.current.politics)
|
||||||
|
return index > -1 ? index : 0
|
||||||
|
},
|
||||||
|
willingPickerIndex() {
|
||||||
|
const options = (this.willingPickerColumns[0] || []).map((item) => item.value)
|
||||||
|
const index = options.indexOf(this.current.willingToWorkInGuangxi)
|
||||||
|
return index > -1 ? index : 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad(options) {
|
||||||
|
this.fixedYear = String(options.year || '').trim()
|
||||||
|
this.pageTitle = decodeURIComponent(options.declareTitle || '青马工程申报').trim() || '青马工程申报'
|
||||||
|
this.loadDictOptions()
|
||||||
|
this.loadData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadDictOptions() {
|
||||||
|
try {
|
||||||
|
const [nationList, politicsList] = await Promise.all([
|
||||||
|
listDictData({ dictId: NATION_DICT_ID }),
|
||||||
|
listDictData({ dictId: POLITICS_DICT_ID })
|
||||||
|
])
|
||||||
|
this.nationOptions = normalizeDictOptions(nationList)
|
||||||
|
this.politicsOptions = normalizeDictOptions(politicsList)
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({ title: (error && error.message) || '字典加载失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadData() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const result = await userPageQmgcForm({
|
||||||
|
page: 1,
|
||||||
|
limit: 50,
|
||||||
|
year: this.fixedYear || undefined
|
||||||
|
})
|
||||||
|
this.list = Array.isArray(result && result.list) ? result.list : []
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({ title: (error && error.message) || '加载失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openCreate() {
|
||||||
|
this.current = { ...createDefaultForm(), year: this.fixedYear || '' }
|
||||||
|
this.showEdit = true
|
||||||
|
},
|
||||||
|
async openEdit(item) {
|
||||||
|
try {
|
||||||
|
const detail = item && item.id ? await getQmgcForm(item.id) : item
|
||||||
|
this.current = { ...createDefaultForm(), ...(detail || {}), year: (detail && detail.year) || this.fixedYear || '' }
|
||||||
|
this.showEdit = true
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({ title: (error && error.message) || '加载详情失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeEdit() { this.showEdit = false },
|
||||||
|
setField(key, event) {
|
||||||
|
const value = typeof event === 'string' ? event : (event.detail && event.detail.value) || ''
|
||||||
|
this.current[key] = value || ''
|
||||||
|
},
|
||||||
|
handleNationChange(event) {
|
||||||
|
const index = Number((event.detail && event.detail.value) || 0)
|
||||||
|
const option = this.nationOptions[index]
|
||||||
|
this.current.nation = (option && option.value) || ''
|
||||||
|
},
|
||||||
|
openGenderPicker() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.genderPicker && this.$refs.genderPicker.open()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
confirmGenderPicker(event) {
|
||||||
|
const value = (((event || {}).value || [])[0] || {}).value || ''
|
||||||
|
this.current.gender = value
|
||||||
|
},
|
||||||
|
openPoliticsPicker() {
|
||||||
|
if (!this.politicsOptions.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.politicsPicker && this.$refs.politicsPicker.open()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
confirmPoliticsPicker(event) {
|
||||||
|
const value = (((event || {}).value || [])[0] || {}).value || ''
|
||||||
|
this.current.politics = value
|
||||||
|
},
|
||||||
|
buildBirthMonthPickerState(value, nextIndexs = []) {
|
||||||
|
const nowYear = new Date().getFullYear()
|
||||||
|
const years = []
|
||||||
|
for (let year = nowYear; year >= 1950; year -= 1) {
|
||||||
|
years.push({ text: `${year}`, value: `${year}` })
|
||||||
|
}
|
||||||
|
const months = Array.from({ length: 12 }, (_, index) => {
|
||||||
|
const month = String(index + 1).padStart(2, '0')
|
||||||
|
return { text: month, value: month }
|
||||||
|
})
|
||||||
|
let selectedYear = String(this.current.birthMonth || '').slice(0, 4) || `${nowYear}`
|
||||||
|
let selectedMonth = String(this.current.birthMonth || '').slice(5, 7) || '01'
|
||||||
|
if (typeof nextIndexs[0] === 'number' && years[nextIndexs[0]]) {
|
||||||
|
selectedYear = years[nextIndexs[0]].value
|
||||||
|
}
|
||||||
|
if (typeof nextIndexs[1] === 'number' && months[nextIndexs[1]]) {
|
||||||
|
selectedMonth = months[nextIndexs[1]].value
|
||||||
|
}
|
||||||
|
const yearIndex = Math.max(0, years.findIndex((item) => item.value === selectedYear))
|
||||||
|
const monthIndex = Math.max(0, months.findIndex((item) => item.value === selectedMonth))
|
||||||
|
return {
|
||||||
|
columns: [years, months],
|
||||||
|
indexs: [yearIndex, monthIndex]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openBirthMonthPicker() {
|
||||||
|
const pickerState = this.buildBirthMonthPickerState(this.current.birthMonth)
|
||||||
|
this.birthMonthPickerColumns = pickerState.columns
|
||||||
|
this.birthMonthPickerIndexs = pickerState.indexs
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.birthMonthPicker && this.$refs.birthMonthPicker.open()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleBirthMonthPickerChange(event) {
|
||||||
|
const pickerState = this.buildBirthMonthPickerState(this.current.birthMonth, (event && event.indexs) || [])
|
||||||
|
this.birthMonthPickerColumns = pickerState.columns
|
||||||
|
this.birthMonthPickerIndexs = pickerState.indexs
|
||||||
|
},
|
||||||
|
confirmBirthMonthPicker(event) {
|
||||||
|
const values = (event && event.value) || []
|
||||||
|
const year = (values[0] && values[0].value) || ''
|
||||||
|
const month = (values[1] && values[1].value) || ''
|
||||||
|
if (year && month) {
|
||||||
|
this.current.birthMonth = `${year}-${month}`
|
||||||
|
}
|
||||||
|
this.birthMonthPickerIndexs = (event && event.indexs) || this.birthMonthPickerIndexs
|
||||||
|
},
|
||||||
|
openWillingPicker() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.willingPicker && this.$refs.willingPicker.open()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
confirmWillingPicker(event) {
|
||||||
|
const values = (event && event.value) || []
|
||||||
|
const option = values[0] || null
|
||||||
|
this.current.willingToWorkInGuangxi = (option && option.value) || ''
|
||||||
|
},
|
||||||
|
async save() {
|
||||||
|
try {
|
||||||
|
const payload = { ...this.current, year: this.fixedYear || this.current.year }
|
||||||
|
if (payload.id) {
|
||||||
|
await updateQmgcForm(payload)
|
||||||
|
} else {
|
||||||
|
await addQmgcForm(payload)
|
||||||
|
}
|
||||||
|
uni.showToast({ title: '保存成功', icon: 'none' })
|
||||||
|
this.closeEdit()
|
||||||
|
this.loadData()
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({ title: (error && error.message) || '保存失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page { min-height: 100vh; background: linear-gradient(180deg, #f5fbff 0%, #eef7ff 100%); }
|
||||||
|
.page-scroll { height: 100vh; padding: 24rpx; box-sizing: border-box; }
|
||||||
|
.hero-card,.state-card,.data-card { border-radius: 28rpx; }
|
||||||
|
.hero-card { padding: 34rpx 36rpx; background: rgba(255,255,255,.5); box-shadow: 0 4rpx 12rpx 0 rgba(174,174,174,.4); backdrop-filter: blur(24rpx); -webkit-backdrop-filter: blur(24rpx); border: 1rpx solid rgba(255,255,255,.5); }
|
||||||
|
.hero-badge { display: inline-flex; padding: 10rpx 18rpx; border-radius: 999rpx; background: rgba(20,150,242,.1); font-size: 22rpx; color: #1496f2; }
|
||||||
|
.hero-title { margin-top: 8rpx; padding-left: 10rpx; font-size: 30rpx; font-weight: 700; line-height: 1.35; color: #18a0f7; border-left: 5px solid #0f94ef; }
|
||||||
|
.hero-desc { margin-top: 20rpx; font-size: 24rpx; line-height: 1.8; color: #7f7f7f; }
|
||||||
|
.hero-actions { display: flex; gap: 16rpx; margin-top: 24rpx; }
|
||||||
|
.hero-action { padding: 14rpx 24rpx; border-radius: 999rpx; background: rgba(20,150,242,.12); font-size: 24rpx; color: #1496f2; }
|
||||||
|
.hero-action--secondary { background: rgba(31,35,41,.08); color: #5f6b76; }
|
||||||
|
.state-card,.data-card { margin-top: 24rpx; padding: 24rpx; background: rgba(255,255,255,.96); box-shadow: 0 10rpx 28rpx rgba(34,94,142,.08); }
|
||||||
|
.state-title,.card-title { font-size: 30rpx; font-weight: 700; color: #1f2329; }
|
||||||
|
.card-subtitle,.meta-line { margin-top: 10rpx; font-size: 24rpx; line-height: 1.6; color: #786e69; }
|
||||||
|
.card-list { margin-top: 24rpx; display: flex; flex-direction: column; gap: 16rpx; }
|
||||||
|
.card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 16rpx; }
|
||||||
|
.card-year { padding: 10rpx 16rpx; border-radius: 999rpx; background: #eef5ff; color: #1554ad; font-size: 22rpx; }
|
||||||
|
.modal-mask { position: fixed; inset: 0; background: rgba(15,23,42,.34); display: flex; align-items: flex-end; z-index: 99; }
|
||||||
|
.modal-panel { width: 100%; max-height: 92vh; border-radius: 32rpx 32rpx 0 0; background: #fff; display: flex; flex-direction: column; }
|
||||||
|
.modal-head { display: flex; justify-content: space-between; align-items: center; padding: 24rpx; border-bottom: 1rpx solid #eef1f4; }
|
||||||
|
.modal-title { font-size: 30rpx; font-weight: 700; color: #1f2329; }
|
||||||
|
.modal-close { font-size: 24rpx; color: #7c8a96; }
|
||||||
|
.modal-body { max-height: 70vh; padding: 24rpx; box-sizing: border-box; }
|
||||||
|
.form-section-title { font-size: 28rpx; font-weight: 700; color: #1f2329; margin-bottom: 16rpx; }
|
||||||
|
.field-item + .field-item { margin-top: 16rpx; }
|
||||||
|
.field-label { margin-bottom: 10rpx; font-size: 24rpx; color: #5f6b7a; }
|
||||||
|
.field-picker { width: 100%; box-sizing: border-box; min-height: 84rpx; padding: 20rpx; border-radius: 20rpx; border: 1rpx solid rgba(220,220,220,1); background: #F4FCFF; font-size: 24rpx; color: #1f2329; }
|
||||||
|
.field-picker--disabled { color: #a8b3bf; background: #f7f9fb; }
|
||||||
|
.field-textarea { width: 100%; box-sizing: border-box; border-radius: 20rpx; border: 1rpx solid #dce4ec; background: #fff; padding: 20rpx; font-size: 24rpx; color: #1f2329; }
|
||||||
|
:deep(.field-input.uv-input) { width: 100%; box-sizing: border-box; border-radius: 20rpx; border: 1rpx solid #dce4ec !important; background: #fff !important; padding: 20rpx !important; }
|
||||||
|
:deep(.field-input.uv-input .uv-input__content__field-wrapper__field) { font-size: 24rpx; color: #1f2329; min-height: 40rpx; }
|
||||||
|
.field-textarea { min-height: 180rpx; }
|
||||||
|
.modal-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16rpx; padding: 24rpx; border-top: 1rpx solid #eef1f4; }
|
||||||
|
.ghost-btn,.primary-btn { height: 84rpx; line-height: 84rpx; border-radius: 22rpx; text-align: center; font-size: 26rpx; font-weight: 700; }
|
||||||
|
.ghost-btn { background: #f6f7fa; color: #50617a; }
|
||||||
|
.primary-btn { background: linear-gradient(135deg, #0f7dd1 0%, #1496f2 100%); color: #fff; }
|
||||||
|
</style>
|
||||||
1297
pages/gxmu/review-list.vue
Normal file
10
pages/gxmu/stats-service.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { API_BASE_URL, apiRequest } from '../assistant/chat-service'
|
||||||
|
|
||||||
|
export function getChallengeCupStatistics(params) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${API_BASE_URL}/gxmu/tzbcy-form/statistics`,
|
||||||
|
method: 'GET',
|
||||||
|
data: params,
|
||||||
|
withSignature: false
|
||||||
|
})
|
||||||
|
}
|
||||||
735
pages/gxmu/stats.vue
Normal file
@@ -0,0 +1,735 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<scroll-view scroll-y class="page-scroll" refresher-enabled :refresher-triggered="refreshing" @refresherrefresh="handleRefresh">
|
||||||
|
<common-hero title="数据统计与报表中心" :use-image-bg="true"></common-hero>
|
||||||
|
<view class="hero-card">
|
||||||
|
<view class="hero-title">挑战杯数据统计</view>
|
||||||
|
<view class="hero-actions">
|
||||||
|
<picker class="picker-wrap" mode="selector" :range="yearOptionLabels" :value="selectedYearIndex" @change="handleYearChange">
|
||||||
|
<view class="action-pill">
|
||||||
|
<text>{{ selectedYear ? `${selectedYear} 年` : '全部年份' }}</text>
|
||||||
|
<text class="action-pill-arrow">▼</text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
<view class="action-pill action-pill--ghost" :class="{ 'action-pill--loading': loading }" @tap="loadData()">
|
||||||
|
{{ loading ? '刷新中' : '刷新数据' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="stats.awardCountDescription" class="alert-card">
|
||||||
|
<view class="alert-icon">i</view>
|
||||||
|
<view class="alert-text">{{ stats.awardCountDescription }}</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading && !hasLoaded" class="state-card">
|
||||||
|
<view class="state-title">正在加载统计数据...</view>
|
||||||
|
<view class="state-desc">请稍候,正在同步挑战杯报表中心数据。</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else-if="loadError" class="state-card state-card--error">
|
||||||
|
<view class="state-title">加载失败</view>
|
||||||
|
<view class="state-desc">{{ loadError }}</view>
|
||||||
|
<view class="state-action" @tap="loadData()">重新加载</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<block v-else>
|
||||||
|
<view class="metrics-grid">
|
||||||
|
<view class="metric-card">
|
||||||
|
<view class="metric-label">申报作品数量</view>
|
||||||
|
<view class="metric-value">{{ summary.declarationCount }}</view>
|
||||||
|
<view class="metric-foot">当前筛选范围内的项目总数</view>
|
||||||
|
</view>
|
||||||
|
<view class="metric-card metric-card--accent">
|
||||||
|
<view class="metric-label">参与申报人数</view>
|
||||||
|
<view class="metric-value">{{ summary.participantCount }}</view>
|
||||||
|
<view class="metric-foot">按团队成员去重统计</view>
|
||||||
|
</view>
|
||||||
|
<view class="metric-card metric-card--warm">
|
||||||
|
<view class="metric-label">指导老师数量</view>
|
||||||
|
<view class="metric-value">{{ summary.advisorCount }}</view>
|
||||||
|
<view class="metric-foot">按指导老师去重统计</view>
|
||||||
|
</view>
|
||||||
|
<view class="metric-card metric-card--success">
|
||||||
|
<view class="metric-label">获奖数量</view>
|
||||||
|
<view class="metric-value">{{ summary.awardCount }}</view>
|
||||||
|
<view class="metric-foot">当前按审核通过作品数统计</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="mini-metrics">
|
||||||
|
<view class="mini-metric-card">
|
||||||
|
<text>申报学校数量</text>
|
||||||
|
<text class="mini-metric-value">{{ summary.schoolCount }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="mini-metric-card">
|
||||||
|
<text>项目类型数</text>
|
||||||
|
<text class="mini-metric-value">{{ stats.typeDistribution.length }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="mini-metric-card">
|
||||||
|
<text>项目分组数</text>
|
||||||
|
<text class="mini-metric-value">{{ stats.groupDistribution.length }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section-card">
|
||||||
|
<view class="section-head">
|
||||||
|
<view>
|
||||||
|
<view class="section-title">年度申报趋势</view>
|
||||||
|
<view class="section-subtitle">展示各年度挑战杯作品申报数量变化</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="stats.yearDistribution.length" class="trend-list">
|
||||||
|
<view v-for="item in stats.yearDistribution" :key="item.label" class="trend-item">
|
||||||
|
<view class="trend-top">
|
||||||
|
<view class="trend-label">{{ item.label }}</view>
|
||||||
|
<view class="trend-value">{{ item.value }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="bar-track">
|
||||||
|
<view class="bar-fill bar-fill--blue" :style="{ width: getBarWidth(item.value, maxYearValue) }"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="empty-card">暂无年度数据</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section-card">
|
||||||
|
<view class="section-head">
|
||||||
|
<view>
|
||||||
|
<view class="section-title">项目类型分布</view>
|
||||||
|
<view class="section-subtitle">按项目类型统计申报数量</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="stats.typeDistribution.length" class="distribution-list">
|
||||||
|
<view v-for="item in stats.typeDistribution" :key="item.label" class="distribution-item">
|
||||||
|
<view class="distribution-main">
|
||||||
|
<view class="distribution-label">{{ item.label }}</view>
|
||||||
|
<view class="distribution-value">{{ item.value }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="bar-track">
|
||||||
|
<view class="bar-fill bar-fill--cyan" :style="{ width: getBarWidth(item.value, maxTypeValue) }"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="empty-card">暂无类型分布</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section-card">
|
||||||
|
<view class="section-head">
|
||||||
|
<view>
|
||||||
|
<view class="section-title">项目分组分布</view>
|
||||||
|
<view class="section-subtitle">按项目分组统计申报数量</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="stats.groupDistribution.length" class="distribution-list">
|
||||||
|
<view v-for="item in stats.groupDistribution" :key="item.label" class="distribution-item">
|
||||||
|
<view class="distribution-main">
|
||||||
|
<view class="distribution-label">{{ item.label }}</view>
|
||||||
|
<view class="distribution-value">{{ item.value }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="bar-track">
|
||||||
|
<view class="bar-fill bar-fill--orange" :style="{ width: getBarWidth(item.value, maxGroupValue) }"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="empty-card">暂无分组分布</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section-card">
|
||||||
|
<view class="section-head">
|
||||||
|
<view>
|
||||||
|
<view class="section-title">学校申报排行</view>
|
||||||
|
<view class="section-subtitle">展示学校维度的申报数量排名</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="stats.schoolDistribution.length" class="ranking-list">
|
||||||
|
<view v-for="(item, index) in stats.schoolDistribution" :key="item.label" class="ranking-item">
|
||||||
|
<view class="ranking-order">{{ index + 1 }}</view>
|
||||||
|
<view class="ranking-main">
|
||||||
|
<view class="distribution-main">
|
||||||
|
<view class="distribution-label">{{ item.label }}</view>
|
||||||
|
<view class="distribution-value">{{ item.value }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="bar-track">
|
||||||
|
<view class="bar-fill bar-fill--green" :style="{ width: getBarWidth(item.value, maxSchoolValue) }"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="empty-card">暂无学校排行</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section-card">
|
||||||
|
<view class="section-head">
|
||||||
|
<view>
|
||||||
|
<view class="section-title">分布明细</view>
|
||||||
|
<view class="section-subtitle">{{ selectedYear ? `${selectedYear} 年` : '全部年份' }}统计口径下的分类数量</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="detail-grid">
|
||||||
|
<view class="detail-panel">
|
||||||
|
<view class="detail-title">项目类型</view>
|
||||||
|
<view v-if="stats.typeDistribution.length" class="detail-list">
|
||||||
|
<view v-for="item in stats.typeDistribution" :key="`type-${item.label}`" class="detail-row">
|
||||||
|
<text class="detail-label">{{ item.label }}</text>
|
||||||
|
<text class="detail-value">{{ item.value }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="detail-empty">暂无数据</view>
|
||||||
|
</view>
|
||||||
|
<view class="detail-panel">
|
||||||
|
<view class="detail-title">项目分组</view>
|
||||||
|
<view v-if="stats.groupDistribution.length" class="detail-list">
|
||||||
|
<view v-for="item in stats.groupDistribution" :key="`group-${item.label}`" class="detail-row">
|
||||||
|
<text class="detail-label">{{ item.label }}</text>
|
||||||
|
<text class="detail-value">{{ item.value }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="detail-empty">暂无数据</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CommonHero from '../../components/common-hero/common-hero.vue'
|
||||||
|
import { getChallengeCupStatistics } from './stats-service'
|
||||||
|
|
||||||
|
const createInitialStats = () => ({
|
||||||
|
availableYears: [],
|
||||||
|
summary: {
|
||||||
|
declarationCount: 0,
|
||||||
|
participantCount: 0,
|
||||||
|
advisorCount: 0,
|
||||||
|
awardCount: 0,
|
||||||
|
schoolCount: 0
|
||||||
|
},
|
||||||
|
awardCountDescription: '',
|
||||||
|
yearDistribution: [],
|
||||||
|
typeDistribution: [],
|
||||||
|
groupDistribution: [],
|
||||||
|
schoolDistribution: []
|
||||||
|
})
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CommonHero
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
refreshing: false,
|
||||||
|
hasLoaded: false,
|
||||||
|
loadError: '',
|
||||||
|
selectedYear: '',
|
||||||
|
stats: createInitialStats()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
summary() {
|
||||||
|
return this.stats.summary || createInitialStats().summary
|
||||||
|
},
|
||||||
|
yearOptionLabels() {
|
||||||
|
const years = Array.isArray(this.stats.availableYears) ? this.stats.availableYears : []
|
||||||
|
return ['全部年份', ...years.map((item) => `${item} 年`)]
|
||||||
|
},
|
||||||
|
selectedYearIndex() {
|
||||||
|
if (!this.selectedYear) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const years = Array.isArray(this.stats.availableYears) ? this.stats.availableYears : []
|
||||||
|
const index = years.findIndex((item) => Number(item) === Number(this.selectedYear))
|
||||||
|
return index >= 0 ? index + 1 : 0
|
||||||
|
},
|
||||||
|
maxYearValue() {
|
||||||
|
return this.getMaxValue(this.stats.yearDistribution)
|
||||||
|
},
|
||||||
|
maxTypeValue() {
|
||||||
|
return this.getMaxValue(this.stats.typeDistribution)
|
||||||
|
},
|
||||||
|
maxGroupValue() {
|
||||||
|
return this.getMaxValue(this.stats.groupDistribution)
|
||||||
|
},
|
||||||
|
maxSchoolValue() {
|
||||||
|
return this.getMaxValue(this.stats.schoolDistribution)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
this.loadData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showToast(title) {
|
||||||
|
uni.showToast({
|
||||||
|
title,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getMaxValue(list) {
|
||||||
|
const values = (Array.isArray(list) ? list : []).map((item) => Number(item.value || 0))
|
||||||
|
return values.length ? Math.max(...values, 1) : 1
|
||||||
|
},
|
||||||
|
getBarWidth(value, maxValue) {
|
||||||
|
const current = Number(value || 0)
|
||||||
|
const max = Number(maxValue || 1)
|
||||||
|
const percent = max > 0 ? (current / max) * 100 : 0
|
||||||
|
return `${Math.max(percent, current > 0 ? 8 : 0)}%`
|
||||||
|
},
|
||||||
|
handleYearChange(event) {
|
||||||
|
const index = Number(event.detail.value || 0)
|
||||||
|
if (index === 0) {
|
||||||
|
this.selectedYear = ''
|
||||||
|
} else {
|
||||||
|
const year = this.stats.availableYears[index - 1]
|
||||||
|
this.selectedYear = year || ''
|
||||||
|
}
|
||||||
|
this.loadData()
|
||||||
|
},
|
||||||
|
handleRefresh() {
|
||||||
|
this.refreshing = true
|
||||||
|
this.loadData(true)
|
||||||
|
},
|
||||||
|
async loadData(fromRefresh = false) {
|
||||||
|
if (this.loading) {
|
||||||
|
if (fromRefresh) {
|
||||||
|
this.refreshing = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.loading = true
|
||||||
|
this.loadError = ''
|
||||||
|
try {
|
||||||
|
const params = this.selectedYear ? { year: Number(this.selectedYear) } : undefined
|
||||||
|
const result = await getChallengeCupStatistics(params)
|
||||||
|
this.stats = {
|
||||||
|
...createInitialStats(),
|
||||||
|
...(result || {}),
|
||||||
|
summary: {
|
||||||
|
...createInitialStats().summary,
|
||||||
|
...((result && result.summary) || {})
|
||||||
|
},
|
||||||
|
availableYears: Array.isArray(result && result.availableYears) ? result.availableYears : [],
|
||||||
|
yearDistribution: Array.isArray(result && result.yearDistribution) ? result.yearDistribution : [],
|
||||||
|
typeDistribution: Array.isArray(result && result.typeDistribution) ? result.typeDistribution : [],
|
||||||
|
groupDistribution: Array.isArray(result && result.groupDistribution) ? result.groupDistribution : [],
|
||||||
|
schoolDistribution: Array.isArray(result && result.schoolDistribution) ? result.schoolDistribution : []
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.selectedYear &&
|
||||||
|
!this.stats.availableYears.some((item) => Number(item) === Number(this.selectedYear))
|
||||||
|
) {
|
||||||
|
this.selectedYear = ''
|
||||||
|
}
|
||||||
|
this.hasLoaded = true
|
||||||
|
} catch (error) {
|
||||||
|
this.loadError = (error && error.message) || '统计数据加载失败'
|
||||||
|
this.showToast(this.loadError)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
if (fromRefresh) {
|
||||||
|
this.refreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(37, 99, 235, 0.14), transparent 28%),
|
||||||
|
linear-gradient(180deg, #f7fbff 0%, #fffdf8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-scroll {
|
||||||
|
height: 100vh;
|
||||||
|
//padding: 28rpx 24rpx 40rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
padding: 34rpx 36rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
box-shadow: 0 4rpx 12rpx 0 rgba(174, 174, 174, 0.4);
|
||||||
|
backdrop-filter: blur(24rpx);
|
||||||
|
-webkit-backdrop-filter: blur(24rpx);
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(20,150,242,.1);
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #1496f2;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
padding-left: 10rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
line-height: 1.35;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #18a0f7;
|
||||||
|
border-left: 5px solid #0f94ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-desc {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #7f7f7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-wrap {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
padding: 16rpx 24rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-pill--ghost {
|
||||||
|
background: rgba(10, 20, 45, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-pill--loading {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-pill-arrow {
|
||||||
|
font-size: 18rpx;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 22rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: #eef6ff;
|
||||||
|
border: 1rpx solid rgba(37, 99, 235, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 36rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #31537c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding: 32rpx 28rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
border: 1rpx solid rgba(37, 99, 235, 0.08);
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(34, 94, 142, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card--error {
|
||||||
|
background: rgba(247, 250, 255, 0.98);
|
||||||
|
border-color: rgba(37, 99, 235, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-desc {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #6c7480;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-action {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 18rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card {
|
||||||
|
padding: 24rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: linear-gradient(135deg, #eff6ff 0%, #ffffff 100%);
|
||||||
|
border: 1rpx solid #dbeafe;
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(15, 23, 42, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card--accent {
|
||||||
|
background: linear-gradient(135deg, #f0fdf4 0%, #ffffff 100%);
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card--warm {
|
||||||
|
background: linear-gradient(135deg, #fff7ed 0%, #ffffff 100%);
|
||||||
|
border-color: #fed7aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card--success {
|
||||||
|
background: linear-gradient(135deg, #ecfeff 0%, #ffffff 100%);
|
||||||
|
border-color: #a5f3fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-value {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 46rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-foot {
|
||||||
|
margin-top: 14rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-metric-card {
|
||||||
|
padding: 22rpx 18rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
box-shadow: 0 8rpx 20rpx rgba(15, 23, 42, 0.05);
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-metric-value {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding: 28rpx 24rpx;
|
||||||
|
border-radius: 30rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
box-shadow: 0 14rpx 34rpx rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #162033;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-subtitle {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #8b94a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-card {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
padding: 28rpx 24rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #f8fbff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #90a0b7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-list,
|
||||||
|
.distribution-list,
|
||||||
|
.ranking-list,
|
||||||
|
.detail-list {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-item,
|
||||||
|
.distribution-item {
|
||||||
|
& + & {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-top,
|
||||||
|
.distribution-main,
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-label,
|
||||||
|
.distribution-label,
|
||||||
|
.detail-label {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #1f2937;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trend-value,
|
||||||
|
.distribution-value,
|
||||||
|
.detail-value {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-track {
|
||||||
|
height: 16rpx;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #edf2f7;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-fill--blue {
|
||||||
|
background: linear-gradient(90deg, #3b82f6 0%, #2563eb 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-fill--cyan {
|
||||||
|
background: linear-gradient(90deg, #38bdf8 0%, #0ea5e9 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-fill--orange {
|
||||||
|
background: linear-gradient(90deg, #fbbf24 0%, #f59e0b 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-fill--green {
|
||||||
|
background: linear-gradient(90deg, #34d399 0%, #10b981 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16rpx;
|
||||||
|
& + & {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking-order {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ecf4ff;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 40rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ranking-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 18rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-panel {
|
||||||
|
padding: 22rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #f9fbfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-title {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
padding: 16rpx 0;
|
||||||
|
border-bottom: 1rpx solid rgba(148, 163, 184, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-empty {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
19
pages/gxmu/tzb-database-service.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { API_BASE_URL, apiRequest } from '../assistant/chat-service'
|
||||||
|
|
||||||
|
export function pageTzbProjectList(params) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${API_BASE_URL}/gxmu/tzb-project-list/page`,
|
||||||
|
method: 'GET',
|
||||||
|
data: params,
|
||||||
|
withSignature: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pageTzbTalentList(params) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${API_BASE_URL}/gxmu/tzb-talent-list/page`,
|
||||||
|
method: 'GET',
|
||||||
|
data: params,
|
||||||
|
withSignature: false
|
||||||
|
})
|
||||||
|
}
|
||||||
205
pages/gxmu/tzb-database.vue
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<scroll-view scroll-y class="page-scroll">
|
||||||
|
<common-hero title="创新竞赛历史项目数据库" :use-image-bg="true"></common-hero>
|
||||||
|
<view class="hero-card">
|
||||||
|
<view class="hero-title">项目库与人才库</view>
|
||||||
|
<view class="hero-desc">
|
||||||
|
沉淀挑战杯历史项目案例、获奖信息与参赛人才画像,支持按项目和人才两个维度快速检索与复用。
|
||||||
|
</view>
|
||||||
|
<!-- <view class="hero-actions">-->
|
||||||
|
<!-- <view class="hero-action" @tap="openPage('/pages/gxmu/tzb-project-list')">进入项目库</view>-->
|
||||||
|
<!-- <view class="hero-action hero-action--secondary" @tap="openPage('/pages/gxmu/tzb-talent-list')">进入人才库</view>-->
|
||||||
|
<!-- </view>-->
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="entry-list">
|
||||||
|
<view class="entry-card" @tap="openPage('/pages/gxmu/tzb-project-list')">
|
||||||
|
<view class="entry-top">
|
||||||
|
<view class="entry-icon">项</view>
|
||||||
|
<view class="entry-tag">项目库</view>
|
||||||
|
</view>
|
||||||
|
<view class="entry-title">挑战杯项目库</view>
|
||||||
|
<view class="entry-desc">查看历届项目名称、学校、奖项、比赛级别、届次与详情链接。</view>
|
||||||
|
<view class="entry-foot">适合做选题借鉴、对标分析与历史案例复盘</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="entry-card" @tap="openPage('/pages/gxmu/tzb-talent-list')">
|
||||||
|
<view class="entry-top">
|
||||||
|
<view class="entry-icon entry-icon--green">才</view>
|
||||||
|
<view class="entry-tag entry-tag--green">人才库</view>
|
||||||
|
</view>
|
||||||
|
<view class="entry-title">挑战杯人才库</view>
|
||||||
|
<view class="entry-desc">查看参赛人才、学校、人才类型、参赛项目、获奖情况与原始详情链接。</view>
|
||||||
|
<view class="entry-foot">适合做导师联络、队伍画像和成果人才线索挖掘</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CommonHero from '../../components/common-hero/common-hero.vue'
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CommonHero
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openPage(url) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(20, 150, 242, 0.14), transparent 30%),
|
||||||
|
linear-gradient(180deg, #f5fbff 0%, #eef7ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-scroll {
|
||||||
|
height: 100vh;
|
||||||
|
padding: 28rpx 24rpx 40rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
padding: 34rpx 36rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
box-shadow: 0 4rpx 12rpx 0 rgba(174, 174, 174, 0.4);
|
||||||
|
backdrop-filter: blur(24rpx);
|
||||||
|
-webkit-backdrop-filter: blur(24rpx);
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(20,150,242,.1);
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #1496f2;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
padding-left: 10rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
line-height: 1.35;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #18a0f7;
|
||||||
|
border-left: 5px solid #0f94ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-desc {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #7f7f7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-action {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 14rpx 24rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(20,150,242,.12);
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1496f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-action--secondary {
|
||||||
|
background: rgba(31,35,41,.08);
|
||||||
|
color: #5f6b76;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-card {
|
||||||
|
padding: 28rpx;
|
||||||
|
border-radius: 30rpx;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(240, 247, 255, 0.95) 0%, #ffffff 44%),
|
||||||
|
#ffffff;
|
||||||
|
border: 1rpx solid rgba(21, 84, 173, 0.08);
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(76, 49, 35, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-icon {
|
||||||
|
width: 76rpx;
|
||||||
|
height: 76rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: linear-gradient(135deg, #1496f2 0%, #4bb7ff 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 76rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-icon--green {
|
||||||
|
background: linear-gradient(135deg, #12b981 0%, #57d5a4 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-tag {
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #edf5ff;
|
||||||
|
color: #1554ad;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-tag--green {
|
||||||
|
background: #e9fbf5;
|
||||||
|
color: #0f9f71;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-title {
|
||||||
|
margin-top: 22rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-desc {
|
||||||
|
margin-top: 14rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #5f6b7a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-foot {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #8b827d;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
661
pages/gxmu/tzb-project-list.vue
Normal file
@@ -0,0 +1,661 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<scroll-view
|
||||||
|
scroll-y
|
||||||
|
class="page-scroll"
|
||||||
|
refresher-enabled
|
||||||
|
:refresher-triggered="refreshing"
|
||||||
|
@refresherrefresh="handleRefresh"
|
||||||
|
@scrolltolower="loadMore"
|
||||||
|
>
|
||||||
|
<common-hero title="挑战杯项目库" :use-image-bg="true"></common-hero>
|
||||||
|
<view class="hero-card">
|
||||||
|
<view class="hero-title">创新竞赛历史项目数据库</view>
|
||||||
|
<view class="hero-desc">
|
||||||
|
按项目名称、学校、奖项、年份和比赛级别查看历届挑战杯案例,支持复制详情链接用于进一步追踪原始页面。
|
||||||
|
</view>
|
||||||
|
<view class="hero-meta">
|
||||||
|
<view class="meta-item">
|
||||||
|
<view class="meta-value">{{ list.length }}</view>
|
||||||
|
<view class="meta-label">当前已加载</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-item">
|
||||||
|
<view class="meta-value">{{ yearOptions.length }}</view>
|
||||||
|
<view class="meta-label">年份筛选</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-item">
|
||||||
|
<view class="meta-value">{{ levelOptions.length }}</view>
|
||||||
|
<view class="meta-label">级别筛选</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="filter-card">
|
||||||
|
<view class="search-box">
|
||||||
|
<uv-input
|
||||||
|
v-model.trim="filters.keywords"
|
||||||
|
class="search-input"
|
||||||
|
placeholder="项目名称/学校/奖项"
|
||||||
|
confirm-type="search"
|
||||||
|
@confirm="applyFilters"
|
||||||
|
:maxlength="-1"
|
||||||
|
border="none"
|
||||||
|
/>
|
||||||
|
<view class="search-btn" @tap="applyFilters">查询</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="field-row">
|
||||||
|
<uv-input
|
||||||
|
v-model.trim="filters.schoolName"
|
||||||
|
class="field-input"
|
||||||
|
placeholder="高校名称"
|
||||||
|
confirm-type="search"
|
||||||
|
@confirm="applyFilters"
|
||||||
|
:maxlength="-1"
|
||||||
|
border="none"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="picker-row">
|
||||||
|
<picker class="picker-item" mode="selector" :range="yearLabels" :value="yearIndex" @change="handleYearChange">
|
||||||
|
<view class="picker-trigger">{{ filters.matchYear ? `${filters.matchYear}年` : '参赛年份' }}</view>
|
||||||
|
</picker>
|
||||||
|
<picker class="picker-item" mode="selector" :range="levelLabels" :value="levelIndex" @change="handleLevelChange">
|
||||||
|
<view class="picker-trigger">{{ filters.matchLevel || '比赛级别' }}</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="action-row">
|
||||||
|
<view class="action-btn action-btn--primary" @tap="applyFilters">查询</view>
|
||||||
|
<view class="action-btn" @tap="resetFilters">重置</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading && !list.length" class="state-card">
|
||||||
|
<view class="state-title">正在加载项目库...</view>
|
||||||
|
<view class="state-desc">请稍候,正在同步挑战杯历史项目数据。</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else-if="loadError && !list.length" class="state-card state-card--error">
|
||||||
|
<view class="state-title">加载失败</view>
|
||||||
|
<view class="state-desc">{{ loadError }}</view>
|
||||||
|
<view class="state-action" @tap="loadData(true)">重新加载</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else-if="!list.length" class="state-card">
|
||||||
|
<view class="state-title">暂无匹配项目</view>
|
||||||
|
<view class="state-desc">请调整搜索条件后重新查询。</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else class="card-list">
|
||||||
|
<view v-for="item in list" :key="item.id || item.sourceId" class="data-card">
|
||||||
|
<view class="card-top">
|
||||||
|
<view class="card-cover" :style="getCoverStyle(item.coverImage)">
|
||||||
|
<view class="cover-tag">{{ item.matchLevel || '未分类' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="card-main">
|
||||||
|
<view class="card-title">{{ item.projectName || '未命名项目' }}</view>
|
||||||
|
<view class="card-school">{{ item.schoolName || '未知学校' }}</view>
|
||||||
|
<view class="tag-row">
|
||||||
|
<view class="tag-chip">{{ item.awardName || '未录入奖项' }}</view>
|
||||||
|
<view class="tag-chip tag-chip--blue">{{ item.matchYear || '-' }}年</view>
|
||||||
|
<view class="tag-chip tag-chip--soft">{{ item.matchTerm || '未知届次' }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="info-grid">
|
||||||
|
<view class="info-item">
|
||||||
|
<view class="info-label">来源ID</view>
|
||||||
|
<view class="info-value">{{ item.sourceId || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<view class="info-label">来源页码</view>
|
||||||
|
<view class="info-value">{{ item.pageNo || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<view class="info-label">同步时间</view>
|
||||||
|
<view class="info-value">{{ formatDateTime(item.lastSyncTime) }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-footer">
|
||||||
|
<view class="footer-link" @tap="copyLink(item.detailUrl)">复制详情链接</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="list.length" class="load-more">
|
||||||
|
<view v-if="loadingMore" class="load-more-text">正在加载更多...</view>
|
||||||
|
<view v-else-if="hasMore" class="load-more-text" @tap="loadMore">点击加载更多</view>
|
||||||
|
<view v-else class="load-more-text load-more-text--end">没有更多数据了</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CommonHero from '../../components/common-hero/common-hero.vue'
|
||||||
|
import { pageTzbProjectList } from './tzb-database-service'
|
||||||
|
|
||||||
|
const DEFAULT_FILTERS = () => ({
|
||||||
|
keywords: '',
|
||||||
|
schoolName: '',
|
||||||
|
matchYear: '',
|
||||||
|
matchLevel: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CommonHero
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
loadingMore: false,
|
||||||
|
refreshing: false,
|
||||||
|
loadError: '',
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
total: 0,
|
||||||
|
hasMore: true,
|
||||||
|
list: [],
|
||||||
|
filters: DEFAULT_FILTERS(),
|
||||||
|
yearOptions: [2025, 2024, 2023, 2022, 2021, 2020, 2019],
|
||||||
|
levelOptions: ['国赛', '省赛', '校赛']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
yearLabels() {
|
||||||
|
return ['全部年份', ...this.yearOptions.map((item) => `${item}年`)]
|
||||||
|
},
|
||||||
|
levelLabels() {
|
||||||
|
return ['全部级别', ...this.levelOptions]
|
||||||
|
},
|
||||||
|
yearIndex() {
|
||||||
|
if (!this.filters.matchYear) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const index = this.yearOptions.findIndex((item) => Number(item) === Number(this.filters.matchYear))
|
||||||
|
return index >= 0 ? index + 1 : 0
|
||||||
|
},
|
||||||
|
levelIndex() {
|
||||||
|
if (!this.filters.matchLevel) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const index = this.levelOptions.findIndex((item) => item === this.filters.matchLevel)
|
||||||
|
return index >= 0 ? index + 1 : 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
this.loadData(true)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showToast(title) {
|
||||||
|
uni.showToast({
|
||||||
|
title,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
formatDateTime(value) {
|
||||||
|
if (!value) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
const date = new Date(String(value).replace(/-/g, '/'))
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
const pad = (item) => String(item).padStart(2, '0')
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||||
|
},
|
||||||
|
getCoverStyle(url) {
|
||||||
|
if (url) {
|
||||||
|
return `background-image: linear-gradient(rgba(15, 23, 42, 0.12), rgba(15, 23, 42, 0.32)), url(${url});`
|
||||||
|
}
|
||||||
|
return 'background: linear-gradient(135deg, #60a5fa 0%, #2563eb 100%);'
|
||||||
|
},
|
||||||
|
handleYearChange(event) {
|
||||||
|
const index = Number(event.detail.value || 0)
|
||||||
|
this.filters.matchYear = index === 0 ? '' : this.yearOptions[index - 1]
|
||||||
|
},
|
||||||
|
handleLevelChange(event) {
|
||||||
|
const index = Number(event.detail.value || 0)
|
||||||
|
this.filters.matchLevel = index === 0 ? '' : this.levelOptions[index - 1]
|
||||||
|
},
|
||||||
|
handleRefresh() {
|
||||||
|
this.refreshing = true
|
||||||
|
this.loadData(true)
|
||||||
|
},
|
||||||
|
applyFilters() {
|
||||||
|
this.loadData(true)
|
||||||
|
},
|
||||||
|
resetFilters() {
|
||||||
|
this.filters = DEFAULT_FILTERS()
|
||||||
|
this.loadData(true)
|
||||||
|
},
|
||||||
|
async loadData(reset = false) {
|
||||||
|
if (this.loading || this.loadingMore) {
|
||||||
|
if (reset) {
|
||||||
|
this.refreshing = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const nextPage = reset ? 1 : this.page
|
||||||
|
if (!reset && !this.hasMore) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (reset) {
|
||||||
|
this.loading = true
|
||||||
|
this.loadError = ''
|
||||||
|
} else {
|
||||||
|
this.loadingMore = true
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: nextPage,
|
||||||
|
limit: this.limit,
|
||||||
|
}
|
||||||
|
if (this.filters.keywords) {
|
||||||
|
params.keywords = this.filters.keywords
|
||||||
|
}
|
||||||
|
if (this.filters.schoolName) {
|
||||||
|
params.schoolName = this.filters.schoolName
|
||||||
|
}
|
||||||
|
if (this.filters.matchYear) {
|
||||||
|
params.matchYear = this.filters.matchYear
|
||||||
|
}
|
||||||
|
if (this.filters.matchLevel) {
|
||||||
|
params.matchLevel = this.filters.matchLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await pageTzbProjectList(params)
|
||||||
|
const nextList = (result && result.list) || []
|
||||||
|
const count = Number((result && result.count) || 0)
|
||||||
|
this.total = count
|
||||||
|
this.page = nextPage + 1
|
||||||
|
this.list = reset ? nextList : this.list.concat(nextList)
|
||||||
|
this.hasMore = this.list.length < count
|
||||||
|
} catch (error) {
|
||||||
|
this.loadError = (error && error.message) || '项目库加载失败'
|
||||||
|
this.showToast(this.loadError)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
this.loadingMore = false
|
||||||
|
this.refreshing = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadMore() {
|
||||||
|
if (!this.loading && !this.loadingMore && this.hasMore) {
|
||||||
|
this.loadData(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
copyLink(url) {
|
||||||
|
if (!url) {
|
||||||
|
this.showToast('暂无详情链接')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: url,
|
||||||
|
success: () => {
|
||||||
|
this.showToast('详情链接已复制')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(37, 99, 235, 0.14), transparent 28%),
|
||||||
|
linear-gradient(180deg, #f7fbff 0%, #fffdf8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-scroll {
|
||||||
|
height: 100vh;
|
||||||
|
//padding: 28rpx 24rpx 40rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card,
|
||||||
|
.filter-card,
|
||||||
|
.state-card,
|
||||||
|
.data-card {
|
||||||
|
border-radius: 30rpx;
|
||||||
|
box-shadow: 0 14rpx 34rpx rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
padding: 34rpx 36rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
box-shadow: 0 4rpx 12rpx 0 rgba(174, 174, 174, 0.4);
|
||||||
|
backdrop-filter: blur(24rpx);
|
||||||
|
-webkit-backdrop-filter: blur(24rpx);
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(20,150,242,.1);
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #1496f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
padding-left: 10rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
line-height: 1.35;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #18a0f7;
|
||||||
|
border-left: 5px solid #0f94ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-desc {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #7f7f7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-meta {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
padding: 18rpx 16rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-label {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: rgba(255, 247, 244, 0.84);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card {
|
||||||
|
margin-top: 22rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box,
|
||||||
|
.field-input,
|
||||||
|
.picker-trigger {
|
||||||
|
background: #f8fbff;
|
||||||
|
border: 1rpx solid rgba(37, 99, 235, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 0 18rpx;
|
||||||
|
height: 84rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.search-input.uv-input) {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.search-input.uv-input .uv-input__content__field-wrapper__field) {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-row,
|
||||||
|
.picker-row,
|
||||||
|
.action-row {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.field-input.uv-input) {
|
||||||
|
width: 100%;
|
||||||
|
height: 84rpx;
|
||||||
|
padding: 0 20rpx !important;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #f8fbff !important;
|
||||||
|
border: 1rpx solid rgba(37, 99, 235, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.field-input.uv-input .uv-input__content__field-wrapper__field) {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-item {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 84rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 82rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: #eef5ff;
|
||||||
|
color: #2563eb;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 82rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn--primary {
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card {
|
||||||
|
margin-top: 22rpx;
|
||||||
|
padding: 32rpx 28rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card--error {
|
||||||
|
background: rgba(247, 250, 255, 0.98);
|
||||||
|
border: 1rpx solid rgba(37, 99, 235, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-desc {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #6c7480;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-action {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18rpx;
|
||||||
|
margin-top: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-card {
|
||||||
|
padding: 24rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-top {
|
||||||
|
display: flex;
|
||||||
|
gap: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-cover {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 168rpx;
|
||||||
|
height: 168rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 16rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-tag {
|
||||||
|
padding: 8rpx 14rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-school {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10rpx;
|
||||||
|
margin-top: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chip {
|
||||||
|
padding: 8rpx 14rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #fff4e6;
|
||||||
|
color: #c76b11;
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chip--blue {
|
||||||
|
background: #eef5ff;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chip--soft {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
padding: 18rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: #f8fbff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #8b94a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 23rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #1f2937;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more {
|
||||||
|
padding: 28rpx 0 12rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-text--end {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
680
pages/gxmu/tzb-talent-list.vue
Normal file
@@ -0,0 +1,680 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<scroll-view
|
||||||
|
scroll-y
|
||||||
|
class="page-scroll"
|
||||||
|
refresher-enabled
|
||||||
|
:refresher-triggered="refreshing"
|
||||||
|
@refresherrefresh="handleRefresh"
|
||||||
|
@scrolltolower="loadMore"
|
||||||
|
>
|
||||||
|
<common-hero title="挑战杯人才库" :use-image-bg="true"></common-hero>
|
||||||
|
<view class="hero-card hero-card--green">
|
||||||
|
<view class="hero-title">创新竞赛历史人才数据库</view>
|
||||||
|
<view class="hero-desc">
|
||||||
|
按姓名、学校、人才类型、参赛项目和比赛级别查看历届挑战杯人才线索,支持复制详情链接用于进一步跟踪。
|
||||||
|
</view>
|
||||||
|
<view class="hero-meta">
|
||||||
|
<view class="meta-item">
|
||||||
|
<view class="meta-value">{{ list.length }}</view>
|
||||||
|
<view class="meta-label">当前已加载</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-item">
|
||||||
|
<view class="meta-value">{{ levelOptions.length }}</view>
|
||||||
|
<view class="meta-label">级别筛选</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-item">
|
||||||
|
<view class="meta-value">{{ talentTypeCount }}</view>
|
||||||
|
<view class="meta-label">人才类型</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="filter-card">
|
||||||
|
<view class="search-box">
|
||||||
|
<uv-input
|
||||||
|
v-model.trim="filters.keywords"
|
||||||
|
class="search-input"
|
||||||
|
placeholder="姓名/项目/学校/奖项"
|
||||||
|
confirm-type="search"
|
||||||
|
@confirm="applyFilters"
|
||||||
|
:maxlength="-1"
|
||||||
|
border="none"
|
||||||
|
/>
|
||||||
|
<view class="search-btn" @tap="applyFilters">查询</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="field-row">
|
||||||
|
<uv-input
|
||||||
|
v-model.trim="filters.schoolName"
|
||||||
|
class="field-input"
|
||||||
|
placeholder="参赛学校"
|
||||||
|
confirm-type="search"
|
||||||
|
@confirm="applyFilters"
|
||||||
|
:maxlength="-1"
|
||||||
|
border="none"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="field-row">
|
||||||
|
<uv-input
|
||||||
|
v-model.trim="filters.talentType"
|
||||||
|
class="field-input"
|
||||||
|
placeholder="人才类型"
|
||||||
|
confirm-type="search"
|
||||||
|
@confirm="applyFilters"
|
||||||
|
:maxlength="-1"
|
||||||
|
border="none"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="picker-row picker-row--single">
|
||||||
|
<picker class="picker-item" mode="selector" :range="levelLabels" :value="levelIndex"
|
||||||
|
@change="handleLevelChange">
|
||||||
|
<view class="picker-trigger">{{ filters.matchLevel || '比赛级别' }}</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="action-row">
|
||||||
|
<view class="action-btn action-btn--primary action-btn--green" @tap="applyFilters">查询</view>
|
||||||
|
<view class="action-btn" @tap="resetFilters">重置</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading && !list.length" class="state-card">
|
||||||
|
<view class="state-title">正在加载人才库...</view>
|
||||||
|
<view class="state-desc">请稍候,正在同步挑战杯历史人才数据。</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else-if="loadError && !list.length" class="state-card state-card--error">
|
||||||
|
<view class="state-title">加载失败</view>
|
||||||
|
<view class="state-desc">{{ loadError }}</view>
|
||||||
|
<view class="state-action state-action--green" @tap="loadData(true)">重新加载</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else-if="!list.length" class="state-card">
|
||||||
|
<view class="state-title">暂无匹配人才</view>
|
||||||
|
<view class="state-desc">请调整搜索条件后重新查询。</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else class="card-list">
|
||||||
|
<view v-for="item in list" :key="item.id || item.sourceId" class="data-card">
|
||||||
|
<view class="card-top">
|
||||||
|
<view class="avatar-cover" :style="getCoverStyle(item.coverImage)">
|
||||||
|
<view class="avatar-text">{{ getAvatarText(item.personName) }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="card-main">
|
||||||
|
<view class="card-title-row">
|
||||||
|
<view class="card-title">{{ item.personName || '未命名人才' }}</view>
|
||||||
|
<view class="tag-chip tag-chip--green">{{ item.talentType || '未分类' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="card-school">{{ item.schoolName || '未知学校' }}</view>
|
||||||
|
<view class="card-project">{{ item.projectName || '未录入项目名称' }}</view>
|
||||||
|
<view class="tag-row">
|
||||||
|
<view class="tag-chip">{{ item.awardName || '未录入奖项' }}</view>
|
||||||
|
<view class="tag-chip tag-chip--blue">{{ item.matchLevel || '未知级别' }}</view>
|
||||||
|
<view class="tag-chip tag-chip--soft">{{ item.matchTerm || '未知届次' }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="info-grid">
|
||||||
|
<view class="info-item">
|
||||||
|
<view class="info-label">来源ID</view>
|
||||||
|
<view class="info-value">{{ item.sourceId || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<view class="info-label">来源页码</view>
|
||||||
|
<view class="info-value">{{ item.pageNo || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<view class="info-label">同步时间</view>
|
||||||
|
<view class="info-value">{{ formatDateTime(item.lastSyncTime) }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-footer">
|
||||||
|
<view class="footer-link footer-link--green" @tap="copyLink(item.detailUrl)">复制详情链接</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="list.length" class="load-more">
|
||||||
|
<view v-if="loadingMore" class="load-more-text">正在加载更多...</view>
|
||||||
|
<view v-else-if="hasMore" class="load-more-text" @tap="loadMore">点击加载更多</view>
|
||||||
|
<view v-else class="load-more-text load-more-text--end">没有更多数据了</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CommonHero from '../../components/common-hero/common-hero.vue'
|
||||||
|
import {pageTzbTalentList} from './tzb-database-service'
|
||||||
|
|
||||||
|
const DEFAULT_FILTERS = () => ({
|
||||||
|
keywords: '',
|
||||||
|
schoolName: '',
|
||||||
|
talentType: '',
|
||||||
|
matchLevel: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CommonHero
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
loadingMore: false,
|
||||||
|
refreshing: false,
|
||||||
|
loadError: '',
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
total: 0,
|
||||||
|
hasMore: true,
|
||||||
|
list: [],
|
||||||
|
filters: DEFAULT_FILTERS(),
|
||||||
|
levelOptions: ['国赛', '省赛', '校赛']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
levelLabels() {
|
||||||
|
return ['全部级别', ...this.levelOptions]
|
||||||
|
},
|
||||||
|
levelIndex() {
|
||||||
|
if (!this.filters.matchLevel) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const index = this.levelOptions.findIndex((item) => item === this.filters.matchLevel)
|
||||||
|
return index >= 0 ? index + 1 : 0
|
||||||
|
},
|
||||||
|
talentTypeCount() {
|
||||||
|
const set = new Set(this.list.map((item) => item.talentType).filter(Boolean))
|
||||||
|
return set.size || '-'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
this.loadData(true)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showToast(title) {
|
||||||
|
uni.showToast({
|
||||||
|
title,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
formatDateTime(value) {
|
||||||
|
if (!value) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
const date = new Date(String(value).replace(/-/g, '/'))
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
const pad = (item) => String(item).padStart(2, '0')
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||||
|
},
|
||||||
|
getCoverStyle(url) {
|
||||||
|
if (url) {
|
||||||
|
return `background-image: linear-gradient(rgba(15, 23, 42, 0.12), rgba(15, 23, 42, 0.24)), url(${url});`
|
||||||
|
}
|
||||||
|
return 'background: linear-gradient(135deg, #34d399 0%, #10b981 100%);'
|
||||||
|
},
|
||||||
|
getAvatarText(name) {
|
||||||
|
return String(name || '人才').slice(-2)
|
||||||
|
},
|
||||||
|
handleLevelChange(event) {
|
||||||
|
const index = Number(event.detail.value || 0)
|
||||||
|
this.filters.matchLevel = index === 0 ? '' : this.levelOptions[index - 1]
|
||||||
|
},
|
||||||
|
handleRefresh() {
|
||||||
|
this.refreshing = true
|
||||||
|
this.loadData(true)
|
||||||
|
},
|
||||||
|
applyFilters() {
|
||||||
|
this.loadData(true)
|
||||||
|
},
|
||||||
|
resetFilters() {
|
||||||
|
this.filters = DEFAULT_FILTERS()
|
||||||
|
this.loadData(true)
|
||||||
|
},
|
||||||
|
async loadData(reset = false) {
|
||||||
|
if (this.loading || this.loadingMore) {
|
||||||
|
if (reset) {
|
||||||
|
this.refreshing = false
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const nextPage = reset ? 1 : this.page
|
||||||
|
if (!reset && !this.hasMore) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (reset) {
|
||||||
|
this.loading = true
|
||||||
|
this.loadError = ''
|
||||||
|
} else {
|
||||||
|
this.loadingMore = true
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: nextPage,
|
||||||
|
limit: this.limit
|
||||||
|
}
|
||||||
|
if (this.filters.keywords) params.keywords = this.filters.keywords
|
||||||
|
if (this.filters.schoolName) params.schoolName = this.filters.schoolName
|
||||||
|
if (this.filters.talentType) params.talentType = this.filters.talentType
|
||||||
|
if (this.filters.matchLevel) params.matchLevel = this.filters.matchLevel
|
||||||
|
const result = await pageTzbTalentList(params)
|
||||||
|
const nextList = (result && result.list) || []
|
||||||
|
const count = Number((result && result.count) || 0)
|
||||||
|
this.total = count
|
||||||
|
this.page = nextPage + 1
|
||||||
|
this.list = reset ? nextList : this.list.concat(nextList)
|
||||||
|
this.hasMore = this.list.length < count
|
||||||
|
} catch (error) {
|
||||||
|
this.loadError = (error && error.message) || '人才库加载失败'
|
||||||
|
this.showToast(this.loadError)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
this.loadingMore = false
|
||||||
|
this.refreshing = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadMore() {
|
||||||
|
if (!this.loading && !this.loadingMore && this.hasMore) {
|
||||||
|
this.loadData(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
copyLink(url) {
|
||||||
|
if (!url) {
|
||||||
|
this.showToast('暂无详情链接')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: url,
|
||||||
|
success: () => {
|
||||||
|
this.showToast('详情链接已复制')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: radial-gradient(circle at top left, rgba(16, 185, 129, 0.14), transparent 28%),
|
||||||
|
linear-gradient(180deg, #f5fffb 0%, #fffdf8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-scroll {
|
||||||
|
height: 100vh;
|
||||||
|
//padding: 28rpx 24rpx 40rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card,
|
||||||
|
.filter-card,
|
||||||
|
.state-card,
|
||||||
|
.data-card {
|
||||||
|
border-radius: 30rpx;
|
||||||
|
box-shadow: 0 14rpx 34rpx rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
padding: 34rpx 36rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
box-shadow: 0 4rpx 12rpx 0 rgba(174, 174, 174, 0.4);
|
||||||
|
backdrop-filter: blur(24rpx);
|
||||||
|
-webkit-backdrop-filter: blur(24rpx);
|
||||||
|
border: 1rpx solid rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card--green .hero-badge {
|
||||||
|
background: rgba(20,150,242,.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #1496f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
padding-left: 10rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
line-height: 1.35;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #18a0f7;
|
||||||
|
border-left: 5px solid #0f94ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-desc {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #7f7f7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-meta {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
padding: 18rpx 16rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-label {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: rgba(247, 255, 251, 0.84);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card {
|
||||||
|
margin-top: 22rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box,
|
||||||
|
.field-input,
|
||||||
|
.picker-trigger {
|
||||||
|
background: #f7fffb;
|
||||||
|
border: 1rpx solid rgba(16, 185, 129, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 0 18rpx;
|
||||||
|
height: 84rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.search-input.uv-input) {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.search-input.uv-input .uv-input__content__field-wrapper__field) {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0f9f71;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-row,
|
||||||
|
.picker-row,
|
||||||
|
.action-row {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.field-input.uv-input) {
|
||||||
|
width: 100%;
|
||||||
|
height: 84rpx;
|
||||||
|
padding: 0 20rpx !important;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #f7fffb !important;
|
||||||
|
border: 1rpx solid rgba(16, 185, 129, 0.12) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.field-input.uv-input .uv-input__content__field-wrapper__field) {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-row--single {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-item {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 84rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 82rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #0f9f71;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 82rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn--primary {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn--green {
|
||||||
|
background: linear-gradient(135deg, #34d399, #10b981);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card {
|
||||||
|
margin-top: 22rpx;
|
||||||
|
padding: 32rpx 28rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card--error {
|
||||||
|
background: rgba(247, 255, 251, 0.98);
|
||||||
|
border: 1rpx solid rgba(16, 185, 129, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-desc {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #6c7480;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-action {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-action--green {
|
||||||
|
color: #0f9f71;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18rpx;
|
||||||
|
margin-top: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-card {
|
||||||
|
padding: 24rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-top {
|
||||||
|
display: flex;
|
||||||
|
gap: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-cover {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 144rpx;
|
||||||
|
height: 144rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-text {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-school,
|
||||||
|
.card-project {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-project {
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10rpx;
|
||||||
|
margin-top: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chip {
|
||||||
|
padding: 8rpx 14rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #fff4e6;
|
||||||
|
color: #c76b11;
|
||||||
|
font-size: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chip--green {
|
||||||
|
background: #e9fbf5;
|
||||||
|
color: #0f9f71;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chip--blue {
|
||||||
|
background: #eef5ff;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-chip--soft {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
padding: 18rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: #f7fffb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #8b94a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 23rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #1f2937;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link--green {
|
||||||
|
color: #0f9f71;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more {
|
||||||
|
padding: 28rpx 0 12rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more-text--end {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
787
pages/gxmu/tzbcy.vue
Normal file
@@ -0,0 +1,787 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<scroll-view scroll-y class="page-scroll">
|
||||||
|
<common-hero :title="pageTitle" :use-image-bg="true"></common-hero>
|
||||||
|
<view class="hero-card">
|
||||||
|
<view class="hero-badge">挑战杯</view>
|
||||||
|
<view class="hero-title">{{ pageTitle }}</view>
|
||||||
|
<view class="hero-desc">按后台完整字段拆分为项目申报表、项目说明、公开展示信息表。</view>
|
||||||
|
<view class="hero-actions">
|
||||||
|
<view class="hero-action" @tap="openCreate">立即申报</view>
|
||||||
|
<view class="hero-action hero-action--secondary" @tap="loadData">刷新列表</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading" class="state-card">
|
||||||
|
<view class="state-title">正在加载申报记录...</view>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="!list.length" class="state-card">
|
||||||
|
<view class="state-title">暂无申报记录</view>
|
||||||
|
<view class="state-desc">点击上方“立即申报”开始填写挑战杯项目。</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="card-list">
|
||||||
|
<view v-for="item in list" :key="item.id" class="data-card" @tap="openEdit(item)">
|
||||||
|
<view class="card-top">
|
||||||
|
<view>
|
||||||
|
<view class="card-title">{{ item.projectName || '未命名项目' }}</view>
|
||||||
|
<view class="card-subtitle">{{ item.projectType || '-' }} · {{ item.projectGroup || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="card-year">{{ item.year || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-line">学校:{{ item.schoolName || '-' }}</view>
|
||||||
|
<view class="meta-line">负责人:{{ getLeaderInfo(item).leader || '-' }} / {{ getLeaderInfo(item).phone || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view v-if="showEdit" class="modal-mask" @tap="closeEdit">
|
||||||
|
<view class="modal-panel" @tap.stop>
|
||||||
|
<view class="modal-head">
|
||||||
|
<view class="modal-title">{{ current.id ? '编辑挑战杯申报' : '新增挑战杯申报' }}</view>
|
||||||
|
<view class="modal-close" @tap="closeEdit">关闭</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="modal-body">
|
||||||
|
<view class="tab-row">
|
||||||
|
<view class="tab-item" :class="{ 'tab-item--active': activeTab === 'base' }" @tap="activeTab = 'base'">项目申报表</view>
|
||||||
|
<view class="tab-item" :class="{ 'tab-item--active': activeTab === 'detail' }" @tap="activeTab = 'detail'">项目说明</view>
|
||||||
|
<view class="tab-item" :class="{ 'tab-item--active': activeTab === 'public' }" @tap="activeTab = 'public'">公开展示信息表</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="activeTab === 'base'" class="form-section">
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">年份</view>
|
||||||
|
<uv-input class="field-input field-input--readonly" :modelValue="current.year" border="none" readonly placeholder="当前申报年份" />
|
||||||
|
</view>
|
||||||
|
<view class="field-grid field-grid--2">
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">所在省(区、市)</view>
|
||||||
|
<uv-input class="field-input" :modelValue="current.provinceCity" border="none" @input="setField('provinceCity', $event)" placeholder="请输入所在省/市" />
|
||||||
|
</view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">学校名称(全称)</view>
|
||||||
|
<uv-input class="field-input" :modelValue="current.schoolName" border="none" @input="setField('schoolName', $event)" placeholder="请输入学校名称" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">项目名称</view>
|
||||||
|
<uv-input class="field-input" :modelValue="current.projectName" border="none" @input="setField('projectName', $event)" placeholder="请输入项目名称" />
|
||||||
|
</view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">项目类型</view>
|
||||||
|
<view class="field-picker" @tap="openFormProjectTypePicker">
|
||||||
|
<text :class="current.projectType ? 'field-value' : 'field-placeholder'">
|
||||||
|
{{ getOptionLabel(current.projectType, formProjectTypeOptions) || '请选择项目类型' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">
|
||||||
|
项目分组
|
||||||
|
<view class="field-note">仅可选择 1 个组别,按后台申报配置提供选项。</view>
|
||||||
|
</view>
|
||||||
|
<view class="field-picker" @tap="openFormProjectGroupPicker">
|
||||||
|
<text :class="current.projectGroup ? 'field-value' : 'field-placeholder'">
|
||||||
|
{{ getOptionLabel(current.projectGroup, formProjectGroupOptions) || '请选择项目分组' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section-header">
|
||||||
|
<view class="section-subtitle">团队成员</view>
|
||||||
|
<view class="section-action" @tap="addTeamMember">新增成员</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="!current.teamMembers.length" class="empty-inline">暂无团队成员,请手动新增。</view>
|
||||||
|
<view v-for="(item, index) in current.teamMembers" :key="`member-${index}`" class="group-card">
|
||||||
|
<view class="group-head">
|
||||||
|
<view class="group-title">成员 {{ index + 1 }}</view>
|
||||||
|
<view class="group-remove" @tap="removeTeamMember(index)">删除</view>
|
||||||
|
</view>
|
||||||
|
<view class="field-grid field-grid--2">
|
||||||
|
<view class="field-item"><view class="field-label">姓名</view><uv-input class="field-input" :modelValue="item.name" border="none" @input="setTeamMemberField(index, 'name', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">性别</view><uv-input class="field-input" :modelValue="item.gender" border="none" @input="setTeamMemberField(index, 'gender', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">学院</view><uv-input class="field-input" :modelValue="item.college" border="none" @input="setTeamMemberField(index, 'college', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">年级、专业</view><uv-input class="field-input" :modelValue="item.gradeMajor" border="none" @input="setTeamMemberField(index, 'gradeMajor', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">手机</view><uv-input class="field-input" :modelValue="item.phone" border="none" @input="setTeamMemberField(index, 'phone', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">备注</view><uv-input class="field-input" :modelValue="item.remark" border="none" @input="setTeamMemberField(index, 'remark', $event)" placeholder="负责人及学历等备注" /></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section-header">
|
||||||
|
<view class="section-subtitle">指导教师</view>
|
||||||
|
<view class="section-action" @tap="addAdvisor">新增教师</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="!current.advisors.length" class="empty-inline">暂无指导教师,请手动新增。</view>
|
||||||
|
<view v-for="(item, index) in current.advisors" :key="`advisor-${index}`" class="group-card">
|
||||||
|
<view class="group-head">
|
||||||
|
<view class="group-title">指导教师 {{ index + 1 }}</view>
|
||||||
|
<view class="group-remove" @tap="removeAdvisor(index)">删除</view>
|
||||||
|
</view>
|
||||||
|
<view class="field-grid field-grid--2">
|
||||||
|
<view class="field-item"><view class="field-label">姓名</view><uv-input class="field-input" :modelValue="item.name" border="none" @input="setAdvisorField(index, 'name', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">性别</view><uv-input class="field-input" :modelValue="item.gender" border="none" @input="setAdvisorField(index, 'gender', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">学院</view><uv-input class="field-input" :modelValue="item.college" border="none" @input="setAdvisorField(index, 'college', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">职称</view><uv-input class="field-input" :modelValue="item.title" border="none" @input="setAdvisorField(index, 'title', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">职务</view><uv-input class="field-input" :modelValue="item.duty" border="none" @input="setAdvisorField(index, 'duty', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">手机</view><uv-input class="field-input" :modelValue="item.phone" border="none" @input="setAdvisorField(index, 'phone', $event)" /></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">项目简介(500 字以内)</view>
|
||||||
|
<textarea class="field-textarea" :value="current.projectBrief" @input="setField('projectBrief', $event)" placeholder="请输入项目简介" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else-if="activeTab === 'detail'" class="form-section">
|
||||||
|
<view class="field-item"><view class="field-label">社会价值(500 字以内)</view><textarea class="field-textarea" :value="current.socialValue" @input="setField('socialValue', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">实践过程(500 字以内)</view><textarea class="field-textarea" :value="current.practiceProcess" @input="setField('practiceProcess', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">创新意义(500 字以内)</view><textarea class="field-textarea" :value="current.innovationMeaning" @input="setField('innovationMeaning', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">发展前景(500 字以内)</view><textarea class="field-textarea" :value="current.developmentProspect" @input="setField('developmentProspect', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">团队协作(500 字以内)</view><textarea class="field-textarea" :value="current.teamCooperation" @input="setField('teamCooperation', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">项目介绍材料</view><textarea class="field-textarea" :value="current.projectMaterials" @input="setField('projectMaterials', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">其他相关证明材料</view><textarea class="field-textarea" :value="current.otherProofs" @input="setField('otherProofs', $event)" placeholder="选报,相关证明材料说明" /></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else class="form-section">
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">项目名称</view>
|
||||||
|
<uv-input class="field-input" :modelValue="current.projectName" border="none" @input="setField('projectName', $event)" placeholder="请输入项目名称" />
|
||||||
|
</view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">参赛学校</view>
|
||||||
|
<uv-input class="field-input" :modelValue="current.schoolName" border="none" @input="setField('schoolName', $event)" placeholder="请输入参赛学校" />
|
||||||
|
</view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">项目类型</view>
|
||||||
|
<view class="field-picker" @tap="openPublicProjectTypePicker">
|
||||||
|
<text :class="current.publicProjectType ? 'field-value' : 'field-placeholder'">
|
||||||
|
{{ getOptionLabel(current.publicProjectType, publicProjectTypeOptions) || '请选择项目类型' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">项目分组</view>
|
||||||
|
<view class="field-picker" @tap="openPublicProjectGroupPicker">
|
||||||
|
<text :class="current.publicProjectGroup ? 'field-value' : 'field-placeholder'">
|
||||||
|
{{ getOptionLabel(current.publicProjectGroup, publicProjectGroupOptions) || '请选择项目分组' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">我们的项目</view>
|
||||||
|
<textarea class="field-textarea" :value="current.projectSummary" @input="setField('projectSummary', $event)" placeholder="150字以内,阐述项目的实践来源、社会背景、基本情况等" />
|
||||||
|
</view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">我们的团队</view>
|
||||||
|
<view class="upload-card">
|
||||||
|
<image v-if="current.teamIntro" class="upload-image" :src="current.teamIntro" mode="aspectFill" @tap="previewImage(current.teamIntro)" />
|
||||||
|
<view v-else class="upload-empty">请上传团队图片</view>
|
||||||
|
<view class="upload-actions">
|
||||||
|
<view class="upload-btn" @tap="chooseTeamImage">上传图片</view>
|
||||||
|
<view v-if="current.teamIntro" class="upload-btn upload-btn--ghost" @tap="clearField('teamIntro')">移除</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="field-item"><view class="field-label">我们的口号</view><uv-input class="field-input" :modelValue="current.teamSlogan" border="none" @input="setField('teamSlogan', $event)" placeholder="20字以内" /></view>
|
||||||
|
<view class="field-item">
|
||||||
|
<view class="field-label">我们的实践日志(选填)</view>
|
||||||
|
<view class="upload-card">
|
||||||
|
<view v-if="current.practiceLog" class="video-box">
|
||||||
|
<video class="upload-video" :src="current.practiceLog" controls object-fit="cover"></video>
|
||||||
|
</view>
|
||||||
|
<view v-else class="upload-empty">请上传实践日志视频</view>
|
||||||
|
<view class="upload-actions">
|
||||||
|
<view class="upload-btn" @tap="choosePracticeVideo">上传视频</view>
|
||||||
|
<view v-if="current.practiceLog" class="upload-btn upload-btn--ghost" @tap="clearField('practiceLog')">移除</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view class="modal-actions">
|
||||||
|
<view class="ghost-btn" @tap="closeEdit">取消</view>
|
||||||
|
<view class="primary-btn" @tap="save">保存</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<uv-picker
|
||||||
|
ref="formProjectTypePicker"
|
||||||
|
title="选择项目类型"
|
||||||
|
:columns="formProjectTypePickerColumns"
|
||||||
|
:defaultIndex="[formProjectTypeIndex]"
|
||||||
|
keyName="text"
|
||||||
|
@close="noop"
|
||||||
|
@cancel="noop"
|
||||||
|
@confirm="confirmFormProjectTypePicker"
|
||||||
|
></uv-picker>
|
||||||
|
|
||||||
|
<uv-picker
|
||||||
|
ref="formProjectGroupPicker"
|
||||||
|
title="选择项目分组"
|
||||||
|
:columns="formProjectGroupPickerColumns"
|
||||||
|
:defaultIndex="[formProjectGroupIndex]"
|
||||||
|
keyName="text"
|
||||||
|
@close="noop"
|
||||||
|
@cancel="noop"
|
||||||
|
@confirm="confirmFormProjectGroupPicker"
|
||||||
|
></uv-picker>
|
||||||
|
|
||||||
|
<uv-picker
|
||||||
|
ref="publicProjectTypePicker"
|
||||||
|
title="选择公开项目类型"
|
||||||
|
:columns="publicProjectTypePickerColumns"
|
||||||
|
:defaultIndex="[publicProjectTypeIndex]"
|
||||||
|
keyName="text"
|
||||||
|
@close="noop"
|
||||||
|
@cancel="noop"
|
||||||
|
@confirm="confirmPublicProjectTypePicker"
|
||||||
|
></uv-picker>
|
||||||
|
|
||||||
|
<uv-picker
|
||||||
|
ref="publicProjectGroupPicker"
|
||||||
|
title="选择公开项目分组"
|
||||||
|
:columns="publicProjectGroupPickerColumns"
|
||||||
|
:defaultIndex="[publicProjectGroupIndex]"
|
||||||
|
keyName="text"
|
||||||
|
@close="noop"
|
||||||
|
@cancel="noop"
|
||||||
|
@confirm="confirmPublicProjectGroupPicker"
|
||||||
|
></uv-picker>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CommonHero from '../../components/common-hero/common-hero.vue'
|
||||||
|
import { addTzbcyForm, getDeclare, getTzbcyForm, uploadTempFile, userPageTzbcyForm } from './module-form-service'
|
||||||
|
|
||||||
|
const FALLBACK_PROJECT_TYPE_OPTIONS = [
|
||||||
|
{ label: 'I. 普通高校', value: '普通高校' },
|
||||||
|
{ label: 'II. 职业院校', value: '职业院校' },
|
||||||
|
{ label: 'III. 东盟留学生专项', value: '东盟留学生专项' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const FALLBACK_PROJECT_GROUP_OPTIONS = [
|
||||||
|
{ label: 'A. 科技创新和未来产业', value: 'A. 科技创新和未来产业' },
|
||||||
|
{ label: 'B. 乡村振兴和农业农村现代化', value: 'B. 乡村振兴和农业农村现代化' },
|
||||||
|
{ label: 'C. 社会治理和公共服务', value: 'C. 社会治理和公共服务' },
|
||||||
|
{ label: 'D. 生态环保和可持续发展', value: 'D. 生态环保和可持续发展' },
|
||||||
|
{ label: 'E. 文化创意和区域合作', value: 'E. 文化创意和区域合作' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const createTeamMember = () => ({
|
||||||
|
name: '',
|
||||||
|
gender: '',
|
||||||
|
college: '',
|
||||||
|
gradeMajor: '',
|
||||||
|
phone: '',
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const createAdvisor = () => ({
|
||||||
|
name: '',
|
||||||
|
gender: '',
|
||||||
|
college: '',
|
||||||
|
title: '',
|
||||||
|
duty: '',
|
||||||
|
phone: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const createDefaultForm = () => ({
|
||||||
|
id: undefined,
|
||||||
|
year: '',
|
||||||
|
provinceCity: '',
|
||||||
|
schoolName: '广西医科大学',
|
||||||
|
projectName: '',
|
||||||
|
projectType: '',
|
||||||
|
projectGroup: '',
|
||||||
|
publicProjectType: '',
|
||||||
|
publicProjectGroup: '',
|
||||||
|
leader: '',
|
||||||
|
phone: '',
|
||||||
|
teamMembers: [],
|
||||||
|
advisors: [],
|
||||||
|
projectBrief: '',
|
||||||
|
socialValue: '',
|
||||||
|
practiceProcess: '',
|
||||||
|
innovationMeaning: '',
|
||||||
|
developmentProspect: '',
|
||||||
|
teamCooperation: '',
|
||||||
|
projectMaterials: '',
|
||||||
|
otherProofs: '',
|
||||||
|
projectSummary: '',
|
||||||
|
teamIntro: '',
|
||||||
|
teamSlogan: '',
|
||||||
|
practiceLog: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const parseMultiValueField = (value) => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) => String(item || '').trim()).filter(Boolean)
|
||||||
|
}
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (!trimmed) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmed)
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
return parsed.map((item) => String(item || '').trim()).filter(Boolean)
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
return [trimmed]
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildChoiceOptions = (customValues, fallback, currentValue) => {
|
||||||
|
const values = (customValues && customValues.length ? customValues : fallback.map((item) => item.value))
|
||||||
|
.map((item) => String(item || '').trim())
|
||||||
|
.filter((item, index, arr) => item && arr.indexOf(item) === index)
|
||||||
|
if (currentValue && String(currentValue).trim() && !values.includes(String(currentValue).trim())) {
|
||||||
|
values.push(String(currentValue).trim())
|
||||||
|
}
|
||||||
|
return values.map((value) => {
|
||||||
|
const matched = fallback.find((item) => item.value === value)
|
||||||
|
return {
|
||||||
|
label: matched && matched.label ? matched.label : value,
|
||||||
|
value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CommonHero
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
initializingDetail: false,
|
||||||
|
showEdit: false,
|
||||||
|
activeTab: 'base',
|
||||||
|
pageTitle: '挑战杯申报',
|
||||||
|
fixedYear: '',
|
||||||
|
declareId: '',
|
||||||
|
list: [],
|
||||||
|
current: createDefaultForm(),
|
||||||
|
declareConfig: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
formProjectTypeOptions() {
|
||||||
|
const config = this.declareConfig || {}
|
||||||
|
return buildChoiceOptions(
|
||||||
|
parseMultiValueField(config.formProjectType || config.projectType),
|
||||||
|
FALLBACK_PROJECT_TYPE_OPTIONS,
|
||||||
|
this.current.projectType
|
||||||
|
)
|
||||||
|
},
|
||||||
|
formProjectGroupOptions() {
|
||||||
|
const config = this.declareConfig || {}
|
||||||
|
return buildChoiceOptions(
|
||||||
|
parseMultiValueField(config.formProjectGroup || config.projectGroup),
|
||||||
|
FALLBACK_PROJECT_GROUP_OPTIONS,
|
||||||
|
this.current.projectGroup
|
||||||
|
)
|
||||||
|
},
|
||||||
|
publicProjectTypeOptions() {
|
||||||
|
const config = this.declareConfig || {}
|
||||||
|
return buildChoiceOptions(
|
||||||
|
parseMultiValueField(config.publicProjectType || config.projectType),
|
||||||
|
FALLBACK_PROJECT_TYPE_OPTIONS,
|
||||||
|
this.current.publicProjectType
|
||||||
|
)
|
||||||
|
},
|
||||||
|
publicProjectGroupOptions() {
|
||||||
|
const config = this.declareConfig || {}
|
||||||
|
return buildChoiceOptions(
|
||||||
|
parseMultiValueField(config.publicProjectGroup || config.projectGroup),
|
||||||
|
FALLBACK_PROJECT_GROUP_OPTIONS,
|
||||||
|
this.current.publicProjectGroup
|
||||||
|
)
|
||||||
|
},
|
||||||
|
formProjectTypePickerColumns() {
|
||||||
|
return [this.formProjectTypeOptions.map((item) => ({ text: item.label, value: item.value }))]
|
||||||
|
},
|
||||||
|
formProjectGroupPickerColumns() {
|
||||||
|
return [this.formProjectGroupOptions.map((item) => ({ text: item.label, value: item.value }))]
|
||||||
|
},
|
||||||
|
publicProjectTypePickerColumns() {
|
||||||
|
return [this.publicProjectTypeOptions.map((item) => ({ text: item.label, value: item.value }))]
|
||||||
|
},
|
||||||
|
publicProjectGroupPickerColumns() {
|
||||||
|
return [this.publicProjectGroupOptions.map((item) => ({ text: item.label, value: item.value }))]
|
||||||
|
},
|
||||||
|
formProjectTypeIndex() {
|
||||||
|
return this.getOptionIndex(this.current.projectType, this.formProjectTypeOptions)
|
||||||
|
},
|
||||||
|
formProjectGroupIndex() {
|
||||||
|
return this.getOptionIndex(this.current.projectGroup, this.formProjectGroupOptions)
|
||||||
|
},
|
||||||
|
publicProjectTypeIndex() {
|
||||||
|
return this.getOptionIndex(this.current.publicProjectType, this.publicProjectTypeOptions)
|
||||||
|
},
|
||||||
|
publicProjectGroupIndex() {
|
||||||
|
return this.getOptionIndex(this.current.publicProjectGroup, this.publicProjectGroupOptions)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad(options) {
|
||||||
|
this.fixedYear = String(options.year || '').trim()
|
||||||
|
this.declareId = String(options.id || '').trim()
|
||||||
|
this.pageTitle = decodeURIComponent(options.declareTitle || '挑战杯申报').trim() || '挑战杯申报'
|
||||||
|
this.loadData()
|
||||||
|
this.loadDeclareConfig()
|
||||||
|
this.loadInitialDetail(options)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getOptionIndex(value, options) {
|
||||||
|
const index = (options || []).findIndex((item) => item.value === value)
|
||||||
|
return index > -1 ? index : 0
|
||||||
|
},
|
||||||
|
getOptionLabel(value, options) {
|
||||||
|
const matched = (options || []).find((item) => item.value === value)
|
||||||
|
return matched ? matched.label : value
|
||||||
|
},
|
||||||
|
normalizeDetail(detail) {
|
||||||
|
const defaults = createDefaultForm()
|
||||||
|
const normalizedProjectType = (detail && detail.projectType) || ''
|
||||||
|
const normalizedProjectGroup = (detail && detail.projectGroup) || ''
|
||||||
|
return {
|
||||||
|
...defaults,
|
||||||
|
...(detail || {}),
|
||||||
|
projectType: normalizedProjectType,
|
||||||
|
projectGroup: normalizedProjectGroup,
|
||||||
|
publicProjectType: (detail && detail.publicProjectType) || normalizedProjectType,
|
||||||
|
publicProjectGroup: (detail && detail.publicProjectGroup) || normalizedProjectGroup,
|
||||||
|
teamMembers: Array.isArray(detail && detail.teamMembers) ? detail.teamMembers.map((item) => ({ ...createTeamMember(), ...(item || {}) })) : [],
|
||||||
|
advisors: Array.isArray(detail && detail.advisors) ? detail.advisors.map((item) => ({ ...createAdvisor(), ...(item || {}) })) : []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getLeaderInfo(record) {
|
||||||
|
const members = Array.isArray(record && record.teamMembers) ? record.teamMembers : []
|
||||||
|
const firstMember = members.find((item) => String(item && (item.name || item.phone) || '').trim())
|
||||||
|
return {
|
||||||
|
leader: (record && record.leader) || (firstMember && firstMember.name) || '',
|
||||||
|
phone: (record && record.phone) || (firstMember && firstMember.phone) || ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadDeclareConfig() {
|
||||||
|
if (!this.declareId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await getDeclare(this.declareId)
|
||||||
|
this.declareConfig = result || null
|
||||||
|
const declareYear = String((result && result.year) || '').trim()
|
||||||
|
if (declareYear) {
|
||||||
|
this.fixedYear = declareYear
|
||||||
|
this.current.year = declareYear
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({ title: (error && error.message) || '申报配置加载失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadInitialDetail(options) {
|
||||||
|
const id = String((options && options.id) || '').trim()
|
||||||
|
if (!id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.initializingDetail = true
|
||||||
|
try {
|
||||||
|
const detail = await getTzbcyForm(id)
|
||||||
|
this.current = {
|
||||||
|
...this.normalizeDetail(detail),
|
||||||
|
year: String((detail && detail.year) || this.fixedYear || '').trim()
|
||||||
|
}
|
||||||
|
if (this.current.year) {
|
||||||
|
this.fixedYear = this.current.year
|
||||||
|
}
|
||||||
|
// this.showEdit = true
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({ title: (error && error.message) || '加载详情失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
this.initializingDetail = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadData() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const result = await userPageTzbcyForm({
|
||||||
|
page: 1,
|
||||||
|
limit: 50,
|
||||||
|
year: this.fixedYear || undefined
|
||||||
|
})
|
||||||
|
this.list = Array.isArray(result && result.list) ? result.list : []
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({ title: (error && error.message) || '加载失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openCreate() {
|
||||||
|
this.current = {
|
||||||
|
...createDefaultForm(),
|
||||||
|
year: this.fixedYear || ''
|
||||||
|
}
|
||||||
|
this.activeTab = 'base'
|
||||||
|
this.showEdit = true
|
||||||
|
},
|
||||||
|
async openEdit(item) {
|
||||||
|
try {
|
||||||
|
const detail = item && item.id ? await getTzbcyForm(item.id) : item
|
||||||
|
this.current = {
|
||||||
|
...this.normalizeDetail(detail),
|
||||||
|
year: String((detail && detail.year) || this.fixedYear || '').trim()
|
||||||
|
}
|
||||||
|
this.activeTab = 'base'
|
||||||
|
this.showEdit = true
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({ title: (error && error.message) || '加载详情失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeEdit() {
|
||||||
|
this.showEdit = false
|
||||||
|
},
|
||||||
|
noop() {
|
||||||
|
return
|
||||||
|
},
|
||||||
|
setField(key, event) {
|
||||||
|
const value = typeof event === 'string' ? event : (event.detail && event.detail.value) || ''
|
||||||
|
this.current[key] = value || ''
|
||||||
|
},
|
||||||
|
clearField(key) {
|
||||||
|
this.current[key] = ''
|
||||||
|
},
|
||||||
|
async chooseTeamImage() {
|
||||||
|
try {
|
||||||
|
const result = await new Promise((resolve, reject) => {
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
sizeType: ['compressed'],
|
||||||
|
sourceType: ['album', 'camera'],
|
||||||
|
success: resolve,
|
||||||
|
fail: reject
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const file = ((result && result.tempFiles) || [])[0]
|
||||||
|
if (!file) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showLoading({ title: '上传中', mask: true })
|
||||||
|
const uploaded = await uploadTempFile(file)
|
||||||
|
this.current.teamIntro = uploaded.url || uploaded.downloadUrl || uploaded.path || ''
|
||||||
|
uni.showToast({ title: '图片上传成功', icon: 'none' })
|
||||||
|
} catch (error) {
|
||||||
|
if (error && error.errMsg && String(error.errMsg).includes('cancel')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showToast({ title: (error && error.message) || '图片上传失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async choosePracticeVideo() {
|
||||||
|
try {
|
||||||
|
const result = await new Promise((resolve, reject) => {
|
||||||
|
uni.chooseVideo({
|
||||||
|
sourceType: ['album', 'camera'],
|
||||||
|
maxDuration: 120,
|
||||||
|
compressed: true,
|
||||||
|
success: resolve,
|
||||||
|
fail: reject
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (!result || !result.tempFilePath) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const file = {
|
||||||
|
path: result.tempFilePath,
|
||||||
|
name: result.name || 'practice-log.mp4',
|
||||||
|
size: result.size || 0
|
||||||
|
}
|
||||||
|
uni.showLoading({ title: '上传中', mask: true })
|
||||||
|
const uploaded = await uploadTempFile(file)
|
||||||
|
this.current.practiceLog = uploaded.url || uploaded.downloadUrl || uploaded.path || ''
|
||||||
|
uni.showToast({ title: '视频上传成功', icon: 'none' })
|
||||||
|
} catch (error) {
|
||||||
|
if (error && error.errMsg && String(error.errMsg).includes('cancel')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showToast({ title: (error && error.message) || '视频上传失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
previewImage(url) {
|
||||||
|
if (!url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.previewImage({
|
||||||
|
urls: [url],
|
||||||
|
current: url
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setTeamMemberField(index, key, event) {
|
||||||
|
const value = typeof event === 'string' ? event : (event.detail && event.detail.value) || ''
|
||||||
|
this.current.teamMembers[index][key] = value || ''
|
||||||
|
},
|
||||||
|
addTeamMember() {
|
||||||
|
this.current.teamMembers = [...this.current.teamMembers, createTeamMember()]
|
||||||
|
},
|
||||||
|
removeTeamMember(index) {
|
||||||
|
this.current.teamMembers = this.current.teamMembers.filter((_, currentIndex) => currentIndex !== index)
|
||||||
|
},
|
||||||
|
setAdvisorField(index, key, event) {
|
||||||
|
const value = typeof event === 'string' ? event : (event.detail && event.detail.value) || ''
|
||||||
|
this.current.advisors[index][key] = value || ''
|
||||||
|
},
|
||||||
|
addAdvisor() {
|
||||||
|
this.current.advisors = [...this.current.advisors, createAdvisor()]
|
||||||
|
},
|
||||||
|
removeAdvisor(index) {
|
||||||
|
this.current.advisors = this.current.advisors.filter((_, currentIndex) => currentIndex !== index)
|
||||||
|
},
|
||||||
|
openFormProjectTypePicker() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.formProjectTypePicker && this.$refs.formProjectTypePicker.open()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
openFormProjectGroupPicker() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.formProjectGroupPicker && this.$refs.formProjectGroupPicker.open()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
openPublicProjectTypePicker() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.publicProjectTypePicker && this.$refs.publicProjectTypePicker.open()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
openPublicProjectGroupPicker() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.publicProjectGroupPicker && this.$refs.publicProjectGroupPicker.open()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
confirmFormProjectTypePicker(event) {
|
||||||
|
const value = (((event || {}).value || [])[0] || {}).value || ''
|
||||||
|
this.current.projectType = value
|
||||||
|
},
|
||||||
|
confirmFormProjectGroupPicker(event) {
|
||||||
|
const value = (((event || {}).value || [])[0] || {}).value || ''
|
||||||
|
this.current.projectGroup = value
|
||||||
|
},
|
||||||
|
confirmPublicProjectTypePicker(event) {
|
||||||
|
const value = (((event || {}).value || [])[0] || {}).value || ''
|
||||||
|
this.current.publicProjectType = value
|
||||||
|
},
|
||||||
|
confirmPublicProjectGroupPicker(event) {
|
||||||
|
const value = (((event || {}).value || [])[0] || {}).value || ''
|
||||||
|
this.current.publicProjectGroup = value
|
||||||
|
},
|
||||||
|
buildSavePayload() {
|
||||||
|
const leaderInfo = this.getLeaderInfo(this.current)
|
||||||
|
return {
|
||||||
|
...this.current,
|
||||||
|
year: this.fixedYear || this.current.year,
|
||||||
|
leader: leaderInfo.leader,
|
||||||
|
phone: leaderInfo.phone
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hasValidTeamMember() {
|
||||||
|
return (this.current.teamMembers || []).some((item) => {
|
||||||
|
return [item.name, item.phone, item.college, item.gradeMajor].some((value) => String(value || '').trim())
|
||||||
|
})
|
||||||
|
},
|
||||||
|
hasValidAdvisor() {
|
||||||
|
return (this.current.advisors || []).some((item) => {
|
||||||
|
return [item.name, item.phone, item.college, item.title, item.duty].some((value) => String(value || '').trim())
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async save() {
|
||||||
|
try {
|
||||||
|
if (!this.hasValidTeamMember()) {
|
||||||
|
uni.showToast({ title: '请至少填写 1 名团队成员', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.hasValidAdvisor()) {
|
||||||
|
uni.showToast({ title: '请至少填写 1 名指导教师', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const payload = this.buildSavePayload()
|
||||||
|
await addTzbcyForm(payload)
|
||||||
|
uni.showToast({ title: '保存成功', icon: 'none' })
|
||||||
|
this.closeEdit()
|
||||||
|
this.loadData()
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({ title: (error && error.message) || '保存失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page { min-height: 100vh; background: linear-gradient(180deg, #f5fbff 0%, #eef7ff 100%); }
|
||||||
|
.page-scroll { height: 100vh; padding: 24rpx; box-sizing: border-box; }
|
||||||
|
.hero-card,.state-card,.data-card { border-radius: 28rpx; }
|
||||||
|
.hero-card { padding: 34rpx 36rpx; background: rgba(255,255,255,.5); box-shadow: 0 4rpx 12rpx 0 rgba(174,174,174,.4); backdrop-filter: blur(24rpx); -webkit-backdrop-filter: blur(24rpx); border: 1rpx solid rgba(255,255,255,.5); }
|
||||||
|
.hero-badge { display: inline-flex; padding: 10rpx 18rpx; border-radius: 999rpx; background: rgba(20,150,242,.1); font-size: 22rpx; color: #1496f2; }
|
||||||
|
.hero-title { margin-top: 8rpx; padding-left: 10rpx; font-size: 30rpx; font-weight: 700; line-height: 1.35; color: #18a0f7; border-left: 5px solid #0f94ef; }
|
||||||
|
.hero-desc { margin-top: 20rpx; font-size: 24rpx; line-height: 1.8; color: #7f7f7f; }
|
||||||
|
.hero-actions { display: flex; gap: 16rpx; margin-top: 24rpx; }
|
||||||
|
.hero-action { padding: 14rpx 24rpx; border-radius: 999rpx; background: rgba(20,150,242,.12); font-size: 24rpx; color: #1496f2; }
|
||||||
|
.hero-action--secondary { background: rgba(31,35,41,.08); color: #5f6b76; }
|
||||||
|
.state-card,.data-card { margin-top: 24rpx; padding: 24rpx; background: rgba(255,255,255,.96); box-shadow: 0 10rpx 28rpx rgba(34,94,142,.08); }
|
||||||
|
.state-title,.card-title { font-size: 30rpx; font-weight: 700; color: #1f2329; }
|
||||||
|
.state-desc,.card-subtitle,.meta-line { margin-top: 10rpx; font-size: 24rpx; line-height: 1.6; color: #786e69; }
|
||||||
|
.card-list { margin-top: 24rpx; display: flex; flex-direction: column; gap: 16rpx; }
|
||||||
|
.card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 16rpx; }
|
||||||
|
.card-year { padding: 10rpx 16rpx; border-radius: 999rpx; background: #eef5ff; color: #1554ad; font-size: 22rpx; }
|
||||||
|
.modal-mask { position: fixed; inset: 0; background: rgba(15,23,42,.34); display: flex; align-items: flex-end; z-index: 99; }
|
||||||
|
.modal-panel { width: 100%; max-height: 92vh; border-radius: 32rpx 32rpx 0 0; background: #fff; display: flex; flex-direction: column; }
|
||||||
|
.modal-head { display: flex; justify-content: space-between; align-items: center; padding: 24rpx; border-bottom: 1rpx solid #eef1f4; }
|
||||||
|
.modal-title { font-size: 30rpx; font-weight: 700; color: #1f2329; }
|
||||||
|
.modal-close { font-size: 24rpx; color: #7c8a96; }
|
||||||
|
.modal-body { max-height: 70vh; padding: 24rpx; box-sizing: border-box; }
|
||||||
|
.tab-row { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12rpx; margin-bottom: 20rpx; }
|
||||||
|
.tab-item { height: 76rpx; line-height: 76rpx; border-radius: 18rpx; background: #eff5ff; color: #1554ad; font-size: 24rpx; text-align: center; }
|
||||||
|
.tab-item--active { background: linear-gradient(135deg, #0f7dd1 0%, #1496f2 100%); color: #fff; }
|
||||||
|
.form-section + .form-section { margin-top: 24rpx; }
|
||||||
|
.form-section-title,.section-subtitle { font-size: 28rpx; font-weight: 700; color: #1f2329; margin-bottom: 16rpx; }
|
||||||
|
.section-header { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; margin-top: 24rpx; }
|
||||||
|
.section-action { flex-shrink: 0; font-size: 22rpx; font-weight: 600; color: #1496f2; }
|
||||||
|
.section-subtitle { margin-top: 24rpx; }
|
||||||
|
.empty-inline { margin-top: 12rpx; padding: 20rpx; border-radius: 16rpx; background: #f8fafc; color: #98a2b3; font-size: 24rpx; text-align: center; }
|
||||||
|
.group-card { margin-top: 16rpx; padding: 20rpx; border-radius: 20rpx; background: #f8fbff; border: 1rpx solid #dce4ec; }
|
||||||
|
.group-head { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; margin-bottom: 16rpx; }
|
||||||
|
.group-title { margin-bottom: 16rpx; font-size: 24rpx; font-weight: 700; color: #1554ad; }
|
||||||
|
.group-remove { flex-shrink: 0; font-size: 22rpx; color: #d92d20; }
|
||||||
|
.field-grid { display: grid; gap: 16rpx; }
|
||||||
|
.field-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
.field-item + .field-item { margin-top: 16rpx; }
|
||||||
|
.field-label { margin-bottom: 10rpx; font-size: 24rpx; color: #5f6b7a; }
|
||||||
|
.field-note { margin-top: 6rpx; font-size: 20rpx; line-height: 1.6; color: #98a2b3; }
|
||||||
|
.field-textarea { width: 100%; box-sizing: border-box; border-radius: 20rpx; border: 1rpx solid #dce4ec; background: #fff; padding: 20rpx; font-size: 24rpx; color: #1f2329; min-height: 180rpx; }
|
||||||
|
:deep(.field-input.uv-input) { width: 100%; box-sizing: border-box; border-radius: 20rpx; border: 1rpx solid #dce4ec !important; background: #fff !important; padding: 20rpx !important; }
|
||||||
|
:deep(.field-input.uv-input .uv-input__content__field-wrapper__field) { font-size: 24rpx; color: #1f2329; min-height: 40rpx; }
|
||||||
|
:deep(.field-input--readonly.uv-input) { background: #f8fafc !important; }
|
||||||
|
:deep(.field-input--readonly.uv-input .uv-input__content__field-wrapper__field) { color: #667085; }
|
||||||
|
.field-picker { width: 100%; box-sizing: border-box; padding: 20rpx; border-radius: 20rpx; border: 1rpx solid rgba(220,220,220,1); background: #F4FCFF; font-size: 24rpx; }
|
||||||
|
.field-placeholder { color: #98a2b3; }
|
||||||
|
.field-value { color: #1f2329; }
|
||||||
|
.upload-card { padding: 20rpx; border-radius: 20rpx; border: 1rpx solid #dce4ec; background: #fff; }
|
||||||
|
.upload-empty { display: flex; align-items: center; justify-content: center; min-height: 220rpx; border-radius: 16rpx; background: #f8fafc; color: #98a2b3; font-size: 24rpx; }
|
||||||
|
.upload-image { width: 100%; height: 280rpx; border-radius: 16rpx; background: #f3f4f6; }
|
||||||
|
.video-box { border-radius: 16rpx; overflow: hidden; background: #0f172a; }
|
||||||
|
.upload-video { width: 100%; height: 320rpx; }
|
||||||
|
.upload-actions { display: flex; gap: 16rpx; margin-top: 18rpx; }
|
||||||
|
.upload-btn { flex: 1; height: 76rpx; line-height: 76rpx; border-radius: 18rpx; background: linear-gradient(135deg, #0f7dd1 0%, #1496f2 100%); color: #fff; text-align: center; font-size: 24rpx; font-weight: 600; }
|
||||||
|
.upload-btn--ghost { background: #eef5ff; color: #1554ad; }
|
||||||
|
.modal-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16rpx; padding: 24rpx; border-top: 1rpx solid #eef1f4; }
|
||||||
|
.ghost-btn,.primary-btn { height: 84rpx; line-height: 84rpx; border-radius: 22rpx; text-align: center; font-size: 26rpx; font-weight: 700; }
|
||||||
|
.ghost-btn { background: #f6f7fa; color: #50617a; }
|
||||||
|
.primary-btn { background: linear-gradient(135deg, #0f7dd1 0%, #1496f2 100%); color: #fff; }
|
||||||
|
</style>
|
||||||
192
pages/gxmu/wxxzx.vue
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<scroll-view scroll-y class="page-scroll">
|
||||||
|
<common-hero :title="pageTitle" :use-image-bg="true"></common-hero>
|
||||||
|
<view class="hero-card">
|
||||||
|
<!-- <view class="hero-badge">GXMU · 未来学术之星</view>-->
|
||||||
|
<view class="hero-title">{{ pageTitle }}</view>
|
||||||
|
<!-- <view class="hero-desc">按照后台未来学术之星申报页结构拆分为基础信息、申请表、立论依据等内容。</view>-->
|
||||||
|
<view class="hero-actions">
|
||||||
|
<view class="hero-action" @tap="openCreate">立即申报</view>
|
||||||
|
<view class="hero-action hero-action--secondary" @tap="loadData">刷新列表</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading" class="state-card"><view class="state-title">正在加载申报记录...</view></view>
|
||||||
|
<view v-else-if="!list.length" class="state-card"><view class="state-title">暂无申报记录</view></view>
|
||||||
|
<view v-else class="card-list">
|
||||||
|
<view v-for="item in list" :key="item.id" class="data-card" @tap="openEdit(item)">
|
||||||
|
<view class="card-top">
|
||||||
|
<view><view class="card-title">{{ item.topicName || '未命名课题' }}</view><view class="card-subtitle">{{ item.collegeGradeClass || '-' }}</view></view>
|
||||||
|
<view class="card-year">{{ item.year || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-line">负责人:{{ item.leader || '-' }}</view>
|
||||||
|
<view class="meta-line">联系电话:{{ item.phone || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<view v-if="showEdit" class="modal-mask" @tap="closeEdit">
|
||||||
|
<view class="modal-panel" @tap.stop>
|
||||||
|
<view class="modal-head"><view class="modal-title">{{ current.id ? '编辑未来学术之星申报' : '新增未来学术之星申报' }}</view><view class="modal-close" @tap="closeEdit">关闭</view></view>
|
||||||
|
<scroll-view scroll-y class="modal-body">
|
||||||
|
<view class="form-section">
|
||||||
|
<view class="form-section-title">基本信息</view>
|
||||||
|
<view class="field-item"><view class="field-label">年份</view><uv-input class="field-input" :modelValue="current.year" border="none" @input="setField('year', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">课题名称</view><uv-input class="field-input" :modelValue="current.topicName" border="none" @input="setField('topicName', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">课题负责人</view><uv-input class="field-input" :modelValue="current.leader" border="none" @input="setField('leader', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">学院、年级、班别</view><uv-input class="field-input" :modelValue="current.collegeGradeClass" border="none" @input="setField('collegeGradeClass', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">联系电话</view><uv-input class="field-input" :modelValue="current.phone" border="none" @input="setField('phone', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">指导老师</view><uv-input class="field-input" :modelValue="current.advisor" border="none" @input="setField('advisor', $event)" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">课题类型</view><uv-input class="field-input" :modelValue="current.topicType" border="none" @input="setField('topicType', $event)" placeholder="自然科学 / 人文科学" /></view>
|
||||||
|
<view class="field-item"><view class="field-label">立论依据</view><textarea class="field-textarea" :value="current.rationale" @input="setField('rationale', $event)" /></view>
|
||||||
|
<!-- <view class="field-item"><view class="field-label">指导老师意见</view><textarea class="field-textarea" :value="current.teacherOpinion" @input="setField('teacherOpinion', $event)" /></view>-->
|
||||||
|
<!-- <view class="field-item"><view class="field-label">指导老师签名</view><uv-input class="field-input" :modelValue="current.teacherSign" border="none" @input="setField('teacherSign', $event)" /></view>-->
|
||||||
|
<!-- <view class="field-item"><view class="field-label">评审小组意见</view><textarea class="field-textarea" :value="current.reviewOpinion" @input="setField('reviewOpinion', $event)" /></view>-->
|
||||||
|
<!-- <view class="field-item"><view class="field-label">评审分数</view><uv-input class="field-input" :modelValue="current.reviewScore" border="none" @input="setField('reviewScore', $event)" /></view>-->
|
||||||
|
<!-- <view class="field-item"><view class="field-label">评审组长签名</view><uv-input class="field-input" :modelValue="current.reviewLeaderSign" border="none" @input="setField('reviewLeaderSign', $event)" /></view>-->
|
||||||
|
<!-- <view class="field-item"><view class="field-label">学院意见</view><textarea class="field-textarea" :value="current.collegeOpinion" @input="setField('collegeOpinion', $event)" /></view>-->
|
||||||
|
<!-- <view class="field-item"><view class="field-label">校团委意见</view><textarea class="field-textarea" :value="current.schoolOpinion" @input="setField('schoolOpinion', $event)" /></view>-->
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="modal-actions"><view class="ghost-btn" @tap="closeEdit">取消</view><view class="primary-btn" @tap="save">保存</view></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CommonHero from '../../components/common-hero/common-hero.vue'
|
||||||
|
import { addWxxzxForm, getWxxzxForm, updateWxxzxForm, userPageWxxzxForm } from './module-form-service'
|
||||||
|
|
||||||
|
const createDefaultForm = () => ({
|
||||||
|
id: undefined,
|
||||||
|
year: '',
|
||||||
|
topicName: '',
|
||||||
|
leader: '',
|
||||||
|
collegeGradeClass: '',
|
||||||
|
phone: '',
|
||||||
|
advisor: '',
|
||||||
|
topicType: '',
|
||||||
|
rationale: '',
|
||||||
|
teacherOpinion: '',
|
||||||
|
teacherSign: '',
|
||||||
|
reviewOpinion: '',
|
||||||
|
reviewScore: '',
|
||||||
|
reviewLeaderSign: '',
|
||||||
|
collegeOpinion: '',
|
||||||
|
schoolOpinion: '',
|
||||||
|
promiseSigner: '',
|
||||||
|
promiseSignature: '',
|
||||||
|
reviewCollege: '',
|
||||||
|
reviewReporter: '',
|
||||||
|
reviewDate: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CommonHero
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
showEdit: false,
|
||||||
|
pageTitle: '未来学术之星申报',
|
||||||
|
fixedYear: '',
|
||||||
|
list: [],
|
||||||
|
current: createDefaultForm()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad(options) {
|
||||||
|
this.fixedYear = String(options.year || '').trim()
|
||||||
|
this.pageTitle = decodeURIComponent(options.declareTitle || '未来学术之星申报').trim() || '未来学术之星申报'
|
||||||
|
this.loadData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadData() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const result = await userPageWxxzxForm({
|
||||||
|
page: 1,
|
||||||
|
limit: 50,
|
||||||
|
year: this.fixedYear || undefined
|
||||||
|
})
|
||||||
|
this.list = Array.isArray(result && result.list) ? result.list : []
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({ title: (error && error.message) || '加载失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openCreate() {
|
||||||
|
this.current = { ...createDefaultForm(), year: this.fixedYear || '' }
|
||||||
|
this.showEdit = true
|
||||||
|
},
|
||||||
|
async openEdit(item) {
|
||||||
|
try {
|
||||||
|
const detail = item && item.id ? await getWxxzxForm(item.id) : item
|
||||||
|
this.current = { ...createDefaultForm(), ...(detail || {}), year: (detail && detail.year) || this.fixedYear || '' }
|
||||||
|
this.showEdit = true
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({ title: (error && error.message) || '加载详情失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeEdit() { this.showEdit = false },
|
||||||
|
setField(key, event) {
|
||||||
|
const value = typeof event === 'string' ? event : (event.detail && event.detail.value) || ''
|
||||||
|
this.current[key] = value || ''
|
||||||
|
},
|
||||||
|
async save() {
|
||||||
|
try {
|
||||||
|
const payload = { ...this.current, year: this.fixedYear || this.current.year }
|
||||||
|
if (payload.id) {
|
||||||
|
await updateWxxzxForm(payload)
|
||||||
|
} else {
|
||||||
|
await addWxxzxForm(payload)
|
||||||
|
}
|
||||||
|
uni.showToast({ title: '保存成功', icon: 'none' })
|
||||||
|
this.closeEdit()
|
||||||
|
this.loadData()
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({ title: (error && error.message) || '保存失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page { min-height: 100vh; background: linear-gradient(180deg, #f5fbff 0%, #eef7ff 100%); }
|
||||||
|
.page-scroll { height: 100vh; padding: 24rpx; box-sizing: border-box; }
|
||||||
|
.hero-card,.state-card,.data-card { border-radius: 28rpx; }
|
||||||
|
.hero-card { padding: 34rpx 36rpx; background: rgba(255,255,255,.5); box-shadow: 0 4rpx 12rpx 0 rgba(174,174,174,.4); backdrop-filter: blur(24rpx); -webkit-backdrop-filter: blur(24rpx); border: 1rpx solid rgba(255,255,255,.5); }
|
||||||
|
.hero-badge { display: inline-flex; padding: 10rpx 18rpx; border-radius: 999rpx; background: rgba(20,150,242,.1); font-size: 22rpx; color: #1496f2; }
|
||||||
|
.hero-title { margin-top: 8rpx; padding-left: 10rpx; font-size: 30rpx; font-weight: 700; line-height: 1.35; color: #18a0f7; border-left: 5px solid #0f94ef; }
|
||||||
|
.hero-desc { margin-top: 20rpx; font-size: 24rpx; line-height: 1.8; color: #7f7f7f; }
|
||||||
|
.hero-actions { display: flex; gap: 16rpx; margin-top: 24rpx; }
|
||||||
|
.hero-action { padding: 14rpx 24rpx; border-radius: 999rpx; background: rgba(20,150,242,.12); font-size: 24rpx; color: #1496f2; }
|
||||||
|
.hero-action--secondary { background: rgba(31,35,41,.08); color: #5f6b76; }
|
||||||
|
.state-card,.data-card { margin-top: 24rpx; padding: 24rpx; background: rgba(255,255,255,.96); box-shadow: 0 10rpx 28rpx rgba(34,94,142,.08); }
|
||||||
|
.state-title,.card-title { font-size: 30rpx; font-weight: 700; color: #1f2329; }
|
||||||
|
.card-subtitle,.meta-line { margin-top: 10rpx; font-size: 24rpx; line-height: 1.6; color: #786e69; }
|
||||||
|
.card-list { margin-top: 24rpx; display: flex; flex-direction: column; gap: 16rpx; }
|
||||||
|
.card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 16rpx; }
|
||||||
|
.card-year { padding: 10rpx 16rpx; border-radius: 999rpx; background: #eef5ff; color: #1554ad; font-size: 22rpx; }
|
||||||
|
.modal-mask { position: fixed; inset: 0; background: rgba(15,23,42,.34); display: flex; align-items: flex-end; z-index: 99; }
|
||||||
|
.modal-panel { width: 100%; max-height: 92vh; border-radius: 32rpx 32rpx 0 0; background: #fff; display: flex; flex-direction: column; }
|
||||||
|
.modal-head { display: flex; justify-content: space-between; align-items: center; padding: 24rpx; border-bottom: 1rpx solid #eef1f4; }
|
||||||
|
.modal-title { font-size: 30rpx; font-weight: 700; color: #1f2329; }
|
||||||
|
.modal-close { font-size: 24rpx; color: #7c8a96; }
|
||||||
|
.modal-body { max-height: 70vh; padding: 24rpx; box-sizing: border-box; }
|
||||||
|
.form-section-title { font-size: 28rpx; font-weight: 700; color: #1f2329; margin-bottom: 16rpx; }
|
||||||
|
.field-item + .field-item { margin-top: 16rpx; }
|
||||||
|
.field-label { margin-bottom: 10rpx; font-size: 24rpx; color: #5f6b7a; }
|
||||||
|
.field-textarea { width: 100%; box-sizing: border-box; border-radius: 20rpx; border: 1rpx solid #dce4ec; background: #fff; padding: 20rpx; font-size: 24rpx; color: #1f2329; }
|
||||||
|
:deep(.field-input.uv-input) { width: 100%; box-sizing: border-box; border-radius: 20rpx; border: 1rpx solid #dce4ec !important; background: #fff !important; padding: 20rpx !important; }
|
||||||
|
:deep(.field-input.uv-input .uv-input__content__field-wrapper__field) { font-size: 24rpx; color: #1f2329; min-height: 40rpx; }
|
||||||
|
.field-textarea { min-height: 180rpx; }
|
||||||
|
.modal-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16rpx; padding: 24rpx; border-top: 1rpx solid #eef1f4; }
|
||||||
|
.ghost-btn,.primary-btn { height: 84rpx; line-height: 84rpx; border-radius: 22rpx; text-align: center; font-size: 26rpx; font-weight: 700; }
|
||||||
|
.ghost-btn { background: #f6f7fa; color: #50617a; }
|
||||||
|
.primary-btn { background: linear-gradient(135deg, #0f7dd1 0%, #1496f2 100%); color: #fff; }
|
||||||
|
</style>
|
||||||
568
pages/index/index.vue
Normal file
@@ -0,0 +1,568 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<scroll-view scroll-y class="page-scroll">
|
||||||
|
<common-hero title="首页" :use-image-bg="true">
|
||||||
|
|
||||||
|
<view class="search-bar" @click="handleSearch">
|
||||||
|
<view class="search-left">
|
||||||
|
<view class="search-icon"></view>
|
||||||
|
<text class="search-placeholder">关键字搜索</text>
|
||||||
|
</view>
|
||||||
|
<text class="search-action">搜索</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<swiper
|
||||||
|
class="hero-swiper"
|
||||||
|
circular
|
||||||
|
autoplay
|
||||||
|
indicator-dots
|
||||||
|
indicator-color="rgba(72, 88, 104, 0.28)"
|
||||||
|
indicator-active-color="#5f6570"
|
||||||
|
interval="3600"
|
||||||
|
duration="500"
|
||||||
|
>
|
||||||
|
<swiper-item v-for="item in banners" :key="item.image">
|
||||||
|
<image class="banner-image" :src="item.image" mode="aspectFill"></image>
|
||||||
|
</swiper-item>
|
||||||
|
</swiper>
|
||||||
|
<view class="notice-card" @click="handleInfoClick">
|
||||||
|
<view class="notice-text">
|
||||||
|
<text class="notice-label">资讯:</text>
|
||||||
|
<text class="notice-content">{{ notice.title }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="notice-arrow">›</view>
|
||||||
|
</view>
|
||||||
|
</common-hero>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">核心业务</view>
|
||||||
|
<view class="business-list">
|
||||||
|
<view
|
||||||
|
v-for="item in featuredModules"
|
||||||
|
:key="item.title"
|
||||||
|
class="business-card"
|
||||||
|
@click="handleFeatureClick(item)"
|
||||||
|
>
|
||||||
|
<view class="business-card-top">
|
||||||
|
<view class="business-icon">{{ item.icon }}</view>
|
||||||
|
<view class="business-tag">{{ item.tag }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="business-title">{{ item.title }}</view>
|
||||||
|
<view class="business-desc">{{ item.desc }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-headline">
|
||||||
|
<view>
|
||||||
|
<view class="section-title">申报系统</view>
|
||||||
|
<view class="section-subtitle">当前开放的申报项目入口</view>
|
||||||
|
</view>
|
||||||
|
<view class="section-link" @click="goDeclareSystem">查看全部</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="declare-entry-card" @click="goDeclareSystem">
|
||||||
|
<view class="declare-entry-top">
|
||||||
|
<view class="declare-entry-action">进入系统</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="declareLoading" class="declare-state">正在加载申报项目...</view>
|
||||||
|
<view v-else-if="declareError" class="declare-state">{{ declareError }}</view>
|
||||||
|
<view v-else-if="!declarePreviewList.length" class="declare-state">当前暂无可申报项目</view>
|
||||||
|
|
||||||
|
<view v-else class="declare-preview-list">
|
||||||
|
<view
|
||||||
|
v-for="item in declarePreviewList"
|
||||||
|
:key="item.id || `${item.module}-${item.year}`"
|
||||||
|
class="declare-preview-item"
|
||||||
|
@click.stop="goDeclareSystem"
|
||||||
|
>
|
||||||
|
<view class="declare-preview-main">
|
||||||
|
<view class="declare-preview-name">{{ getDeclareTitle(item) }}</view>
|
||||||
|
<view class="declare-preview-meta">
|
||||||
|
{{ getDeclareGroup(item.module) }} · {{ item.year ? `${item.year}年` : '未设置年度' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="declare-preview-tag">可申报</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CommonHero from '../../components/common-hero/common-hero.vue'
|
||||||
|
import { getFormConfig, getModuleMeta } from '../../utils/gxmu/config'
|
||||||
|
import { listDeclare } from '../../utils/gxmu/declare-service'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CommonHero
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
banners: [
|
||||||
|
{
|
||||||
|
image: '/static/swiper.jpg'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
notice: {
|
||||||
|
title: '文稿、长图、排版和素材能力统一沉淀'
|
||||||
|
},
|
||||||
|
declareLoading: false,
|
||||||
|
declareError: '',
|
||||||
|
declareList: [],
|
||||||
|
featuredModules: [
|
||||||
|
{
|
||||||
|
title: '团员档案管理',
|
||||||
|
desc: '成员信息、组织关系、成长记录',
|
||||||
|
icon: '档',
|
||||||
|
tag: '组织',
|
||||||
|
route: '/pages/tygl/index'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '数据统计与报表中心',
|
||||||
|
desc: '查看挑战杯项目的年度趋势、结构分布与学校排行。',
|
||||||
|
icon: '数',
|
||||||
|
tag: '报表',
|
||||||
|
route: '/pages/gxmu/stats'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '跨校活动情报看板',
|
||||||
|
desc: '聚合兄弟高校案例线索,快速对标活动策划与传播打法。',
|
||||||
|
icon: '情',
|
||||||
|
tag: '案例',
|
||||||
|
route: '/pages/gxmu/cross-school-board'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创新竞赛历史项目数据库',
|
||||||
|
desc: '进入挑战杯项目库与人才库,检索历史案例、人才与详情链接。',
|
||||||
|
icon: '库',
|
||||||
|
tag: '数据',
|
||||||
|
route: '/pages/gxmu/tzb-database'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
declarePreviewList() {
|
||||||
|
return this.declareList.slice(0, 2)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
this.loadDeclareList()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getModuleMetaSafe(code) {
|
||||||
|
return getModuleMeta(code) || {
|
||||||
|
title: code || '未命名模块',
|
||||||
|
group: '申报项目'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getDeclareTitle(item) {
|
||||||
|
const title = String((item && item.title) || '').trim()
|
||||||
|
if (title) {
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
return this.getModuleMetaSafe(item && item.module).title
|
||||||
|
},
|
||||||
|
getDeclareGroup(module) {
|
||||||
|
return this.getModuleMetaSafe(module).group
|
||||||
|
},
|
||||||
|
getTimestamp(value) {
|
||||||
|
if (!value) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const time = new Date(String(value).replace(/-/g, '/')).getTime()
|
||||||
|
return Number.isNaN(time) ? 0 : time
|
||||||
|
},
|
||||||
|
async loadDeclareList() {
|
||||||
|
if (this.declareLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.declareLoading = true
|
||||||
|
this.declareError = ''
|
||||||
|
try {
|
||||||
|
const result = await listDeclare({ usable: true })
|
||||||
|
const list = Array.isArray(result) ? result : []
|
||||||
|
this.declareList = list
|
||||||
|
.sort((left, right) => {
|
||||||
|
const yearDiff = Number(right.year || 0) - Number(left.year || 0)
|
||||||
|
if (yearDiff !== 0) {
|
||||||
|
return yearDiff
|
||||||
|
}
|
||||||
|
return this.getTimestamp(right.startTime || right.createTime) - this.getTimestamp(left.startTime || left.createTime)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
this.declareError = (error && error.message) || '申报项目加载失败'
|
||||||
|
} finally {
|
||||||
|
this.declareLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
uni.showToast({
|
||||||
|
title: '搜索功能开发中',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleInfoClick() {
|
||||||
|
uni.showToast({
|
||||||
|
title: this.notice.title,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleFeatureClick(item) {
|
||||||
|
if (item.route) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: item.route
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: `${item.title}待接入`,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goDeclareSystem() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/gxmu/index'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(180deg, #f8fbff 0%, #edf3f8 32%, #f7f7f8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-scroll {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
position: absolute;
|
||||||
|
top: 170rpx;
|
||||||
|
z-index: 9999;
|
||||||
|
width: calc(100vw - 56rpx);
|
||||||
|
left: 28rpx;
|
||||||
|
padding: 0 26rpx 0 30rpx;
|
||||||
|
height: 70rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.97);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-shadow: 0 16rpx 40rpx rgba(97, 151, 193, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
width: 26rpx;
|
||||||
|
height: 26rpx;
|
||||||
|
border: 4rpx solid #c7c7c7;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: -10rpx;
|
||||||
|
bottom: -8rpx;
|
||||||
|
width: 14rpx;
|
||||||
|
height: 4rpx;
|
||||||
|
background: #c7c7c7;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-placeholder {
|
||||||
|
margin-left: 20rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #cbcbcb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-action {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-swiper {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
height: 338rpx;
|
||||||
|
margin-top: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-image {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 338rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-card {
|
||||||
|
margin: 18rpx 28rpx 0;
|
||||||
|
padding: 0 20rpx 0 28rpx;
|
||||||
|
height: 86rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.98);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-shadow: 0 12rpx 32rpx rgba(55, 95, 130, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1197ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-content {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-arrow {
|
||||||
|
width: 42rpx;
|
||||||
|
height: 42rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2rpx solid #4ca7ff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #4ca7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 20rpx 28rpx 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-headline {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 46rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #15a0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-subtitle {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #7c8a96;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-link {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1496f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.business-list {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.business-card + .business-card {
|
||||||
|
margin-top: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.business-card {
|
||||||
|
padding: 24rpx 22rpx 26rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.98);
|
||||||
|
box-shadow: 0 20rpx 48rpx rgba(79, 98, 117, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.business-card-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.business-icon {
|
||||||
|
min-width: 88rpx;
|
||||||
|
height: 58rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: linear-gradient(135deg, #16b2ff 0%, #1496f2 100%);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.business-tag {
|
||||||
|
min-width: 90rpx;
|
||||||
|
height: 42rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
border: 2rpx solid #22a4ff;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #22a4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.business-title {
|
||||||
|
margin-top: 22rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.business-desc {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #7c7c7c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.declare-entry-card {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding: 28rpx 24rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(240, 247, 255, 0.96) 0%, rgba(255, 255, 255, 0.98) 44%),
|
||||||
|
#ffffff;
|
||||||
|
box-shadow: 0 20rpx 48rpx rgba(79, 98, 117, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.declare-entry-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.declare-entry-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 46rpx;
|
||||||
|
padding: 0 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #eaf4ff;
|
||||||
|
color: #1554ad;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.declare-entry-action {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1496f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.declare-entry-title {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: #1f2329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.declare-entry-desc {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #607080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.declare-state {
|
||||||
|
margin-top: 22rpx;
|
||||||
|
padding: 20rpx 22rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
background: rgba(246, 248, 250, 0.9);
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #7d8a97;
|
||||||
|
}
|
||||||
|
|
||||||
|
.declare-preview-list {
|
||||||
|
margin-top: 22rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.declare-preview-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
border: 1rpx solid rgba(20, 150, 242, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.declare-preview-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.declare-preview-name {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: #1f2329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.declare-preview-meta {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #7c8a96;
|
||||||
|
}
|
||||||
|
|
||||||
|
.declare-preview-tag {
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 42rpx;
|
||||||
|
padding: 0 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #eef5ff;
|
||||||
|
color: #1554ad;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 42rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
298
pages/login/index.vue
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<view class="hero-card">
|
||||||
|
<view class="hero-title">进入智慧团委小程序</view>
|
||||||
|
<view class="hero-desc">支持手机号密码登录,也支持微信手机号授权登录。</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="login-card">
|
||||||
|
<view class="tab-row">
|
||||||
|
<view
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ 'tab-item--active': loginMode === 'password' }"
|
||||||
|
@tap="loginMode = 'password'"
|
||||||
|
>
|
||||||
|
手机号密码登录
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ 'tab-item--active': loginMode === 'wechat' }"
|
||||||
|
@tap="loginMode = 'wechat'"
|
||||||
|
>
|
||||||
|
微信授权登录
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loginMode === 'password'" class="form-section">
|
||||||
|
<uv-input
|
||||||
|
v-model.trim="form.username"
|
||||||
|
class="field-input"
|
||||||
|
type="text"
|
||||||
|
placeholder="请输入手机号或账号"
|
||||||
|
:maxlength="-1"
|
||||||
|
border="none"
|
||||||
|
/>
|
||||||
|
<uv-input
|
||||||
|
v-model="form.password"
|
||||||
|
class="field-input"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
:maxlength="-1"
|
||||||
|
border="none"
|
||||||
|
password
|
||||||
|
/>
|
||||||
|
<view class="submit-btn" :class="{ 'submit-btn--loading': loading }" @tap="submitPasswordLogin">
|
||||||
|
{{ loading ? '登录中...' : '立即登录' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else class="form-section">
|
||||||
|
<view class="wechat-intro">授权后将使用微信手机号完成登录。</view>
|
||||||
|
<button
|
||||||
|
class="submit-btn submit-btn--wechat"
|
||||||
|
:disabled="loading"
|
||||||
|
open-type="getPhoneNumber"
|
||||||
|
@getphonenumber="handleWxPhoneLogin"
|
||||||
|
>
|
||||||
|
{{ loading ? '登录中...' : '微信授权登录' }}
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { loginByMpWxPhone, loginByPassword } from './service'
|
||||||
|
import { hasToken, setAuthInfo } from '../../utils/request'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loginMode: 'password',
|
||||||
|
loading: false,
|
||||||
|
redirect: '',
|
||||||
|
wxCode: '',
|
||||||
|
form: {
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad(options) {
|
||||||
|
this.redirect = decodeURIComponent((options && options.redirect) || '')
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
if (hasToken()) {
|
||||||
|
this.navigateAfterLogin()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.refreshWxCode()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showToast(title) {
|
||||||
|
uni.showToast({
|
||||||
|
title,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
refreshWxCode() {
|
||||||
|
uni.login({
|
||||||
|
success: (res) => {
|
||||||
|
this.wxCode = (res && res.code) || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
navigateAfterLogin() {
|
||||||
|
const url = this.redirect || '/pages/index/index'
|
||||||
|
uni.reLaunch({
|
||||||
|
url
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async submitPasswordLogin() {
|
||||||
|
if (this.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.form.username) {
|
||||||
|
this.showToast('请输入手机号或账号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.form.password) {
|
||||||
|
this.showToast('请输入密码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const result = await loginByPassword({
|
||||||
|
username: this.form.username,
|
||||||
|
password: this.form.password
|
||||||
|
})
|
||||||
|
setAuthInfo(result)
|
||||||
|
this.showToast('登录成功')
|
||||||
|
setTimeout(() => {
|
||||||
|
this.navigateAfterLogin()
|
||||||
|
}, 300)
|
||||||
|
} catch (error) {
|
||||||
|
this.showToast((error && error.message) || '登录失败')
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async handleWxPhoneLogin(event) {
|
||||||
|
if (this.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const detail = (event && event.detail) || {}
|
||||||
|
if (!detail.code) {
|
||||||
|
this.showToast(detail.errMsg || '未获取到微信手机号凭证')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
if (!this.wxCode) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
uni.login({
|
||||||
|
success: (res) => {
|
||||||
|
this.wxCode = (res && res.code) || ''
|
||||||
|
resolve()
|
||||||
|
},
|
||||||
|
fail: () => resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const result = await loginByMpWxPhone({
|
||||||
|
phoneCode: detail.code,
|
||||||
|
code: this.wxCode || undefined
|
||||||
|
})
|
||||||
|
setAuthInfo(result)
|
||||||
|
this.showToast('登录成功')
|
||||||
|
setTimeout(() => {
|
||||||
|
this.navigateAfterLogin()
|
||||||
|
}, 300)
|
||||||
|
} catch (error) {
|
||||||
|
this.showToast((error && error.message) || '微信登录失败')
|
||||||
|
this.refreshWxCode()
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 28rpx 24rpx 40rpx;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(20, 150, 242, 0.14), transparent 30%),
|
||||||
|
linear-gradient(180deg, #f5fbff 0%, #eef7ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card,
|
||||||
|
.login-card {
|
||||||
|
border-radius: 32rpx;
|
||||||
|
box-shadow: 0 18rpx 40rpx rgba(20, 118, 194, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
padding: 34rpx 30rpx;
|
||||||
|
background: linear-gradient(145deg, #0d6fba 0%, #1496f2 58%, #5bc2ff 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.16);
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
margin-top: 22rpx;
|
||||||
|
font-size: 42rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-desc {
|
||||||
|
margin-top: 14rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: rgba(255, 255, 255, 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding: 28rpx 24rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
height: 82rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: #edf5ff;
|
||||||
|
color: #1496f2;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 82rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item--active {
|
||||||
|
background: linear-gradient(135deg, #1496f2, #0d6fba);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-top: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.field-input.uv-input) {
|
||||||
|
width: 100%;
|
||||||
|
height: 88rpx;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
padding: 0 24rpx !important;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #f8fbff !important;
|
||||||
|
border: 1rpx solid rgba(20, 150, 242, 0.12) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.field-input.uv-input .uv-input__content__field-wrapper__field) {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #1f2329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wechat-intro {
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #667085;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 88rpx;
|
||||||
|
margin-top: 22rpx;
|
||||||
|
border: none;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: linear-gradient(135deg, #1496f2, #0d6fba);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 88rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn--wechat {
|
||||||
|
background: linear-gradient(135deg, #1dbf73, #12a150);
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn--loading {
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
21
pages/login/service.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { DEFAULT_TENANT_ID, post } from '../../utils/request'
|
||||||
|
|
||||||
|
export function loginByPassword(data) {
|
||||||
|
return post(
|
||||||
|
'/login',
|
||||||
|
{
|
||||||
|
tenantId: DEFAULT_TENANT_ID,
|
||||||
|
isMiniApp: 1,
|
||||||
|
...data
|
||||||
|
},
|
||||||
|
{
|
||||||
|
auth: false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loginByMpWxPhone(data) {
|
||||||
|
return post('/wx-login/loginByMpWxPhone', data, {
|
||||||
|
auth: false
|
||||||
|
})
|
||||||
|
}
|
||||||
1112
pages/manuscript/index.vue
Normal file
52
pages/manuscript/service.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
API_BASE_URL,
|
||||||
|
apiRequest,
|
||||||
|
buildWebSocketUrl,
|
||||||
|
getCurrentUser,
|
||||||
|
getToken
|
||||||
|
} from '../assistant/chat-service'
|
||||||
|
|
||||||
|
export { buildWebSocketUrl, getCurrentUser }
|
||||||
|
|
||||||
|
export function manuscriptGen(data) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${API_BASE_URL}/ai/manuscriptGen`,
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
withTenant: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uploadTempFile(file) {
|
||||||
|
const token = getToken()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.uploadFile({
|
||||||
|
url: `${API_BASE_URL}/file/upload`,
|
||||||
|
filePath: file.path,
|
||||||
|
name: 'file',
|
||||||
|
header: token
|
||||||
|
? {
|
||||||
|
Authorization: token
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
formData: {
|
||||||
|
tenantId: '10049'
|
||||||
|
},
|
||||||
|
success: (response) => {
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(response.data || '{}')
|
||||||
|
if (result.code === 0 && result.data) {
|
||||||
|
resolve(result.data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reject(new Error(result.message || '上传失败'))
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error('上传响应解析失败'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
reject(new Error(error.errMsg || '上传失败'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
355
pages/mine/declare.vue
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<scroll-view scroll-y class="page-scroll">
|
||||||
|
<view class="hero-card">
|
||||||
|
<view class="hero-title">我的申报</view>
|
||||||
|
<view class="hero-desc">汇总本地草稿、已提交记录和当前开放的申报项目。</view>
|
||||||
|
<view class="hero-stats">
|
||||||
|
<view class="stat-item">
|
||||||
|
<view class="stat-value">{{ localRecords.length }}</view>
|
||||||
|
<view class="stat-label">本地记录</view>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<view class="stat-value">{{ submittedCount }}</view>
|
||||||
|
<view class="stat-label">已提交</view>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<view class="stat-value">{{ openDeclareList.length }}</view>
|
||||||
|
<view class="stat-label">开放项目</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section-card">
|
||||||
|
<view class="section-head">
|
||||||
|
<view class="section-title">本地申报记录</view>
|
||||||
|
<view class="section-action" @tap="loadData()">刷新</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="!localRecords.length" class="empty-block">
|
||||||
|
当前还没有本地草稿或提交记录,可先前往申报页填写。
|
||||||
|
</view>
|
||||||
|
<view v-else class="card-list">
|
||||||
|
<view v-for="item in localRecords" :key="item.key" class="data-card">
|
||||||
|
<view class="card-top">
|
||||||
|
<view class="card-main">
|
||||||
|
<view class="card-title">{{ item.title }}</view>
|
||||||
|
<view class="card-subtitle">{{ item.year || '未设置年度' }} · {{ item.group }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="status-tag" :class="item.status === '已提交' ? 'status-tag--success' : 'status-tag--warning'">
|
||||||
|
{{ item.status }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-row">保存时间:{{ item.savedAt || '-' }}</view>
|
||||||
|
<view class="card-actions">
|
||||||
|
<view class="card-action" @tap="continueEdit(item)">继续编辑</view>
|
||||||
|
<view class="card-action" @tap="copyText(item.title)">复制标题</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section-card">
|
||||||
|
<view class="section-head">
|
||||||
|
<view class="section-title">当前开放项目</view>
|
||||||
|
<view class="section-action" @tap="goDeclareList">全部查看</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="loading" class="empty-block">正在加载开放项目...</view>
|
||||||
|
<view v-else-if="errorText" class="empty-block empty-block--error">{{ errorText }}</view>
|
||||||
|
<view v-else-if="!openDeclareList.length" class="empty-block">当前暂无开放的申报项目。</view>
|
||||||
|
<view v-else class="card-list">
|
||||||
|
<view v-for="item in openDeclareList" :key="item.id || `${item.module}-${item.year}`" class="data-card">
|
||||||
|
<view class="card-top">
|
||||||
|
<view class="card-main">
|
||||||
|
<view class="card-title">{{ getDeclareTitle(item) }}</view>
|
||||||
|
<view class="card-subtitle">{{ getDeclareGroup(item.module) }} · {{ item.year || '未设置年度' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="status-tag status-tag--default">可申报</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-row">时间范围:{{ formatDateTime(item.startTime) }} 至 {{ formatDateTime(item.endTime) }}</view>
|
||||||
|
<view class="card-actions">
|
||||||
|
<view class="card-action" @tap="goApply(item)">进入申报</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
formatDateTime,
|
||||||
|
getLocalDeclareRecords,
|
||||||
|
listAvailableDeclare
|
||||||
|
} from './service'
|
||||||
|
import { getFormConfig, getModuleMeta } from '../../utils/gxmu/config'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
errorText: '',
|
||||||
|
localRecords: [],
|
||||||
|
openDeclareList: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
submittedCount() {
|
||||||
|
return this.localRecords.filter((item) => item.status === '已提交').length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
this.loadData()
|
||||||
|
},
|
||||||
|
onPullDownRefresh() {
|
||||||
|
this.loadData(true)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatDateTime,
|
||||||
|
getDeclareTitle(item) {
|
||||||
|
return String(item.title || '').trim() || this.getDeclareGroup(item.module)
|
||||||
|
},
|
||||||
|
getDeclareGroup(module) {
|
||||||
|
const meta = getModuleMeta(module)
|
||||||
|
return (meta && meta.title) || module || '申报项目'
|
||||||
|
},
|
||||||
|
async loadData(fromPullDown = false) {
|
||||||
|
this.localRecords = getLocalDeclareRecords()
|
||||||
|
this.loading = true
|
||||||
|
this.errorText = ''
|
||||||
|
try {
|
||||||
|
const result = await listAvailableDeclare({ usable: true })
|
||||||
|
const list = Array.isArray(result) ? result : []
|
||||||
|
this.openDeclareList = list.sort((left, right) => Number(right.year || 0) - Number(left.year || 0))
|
||||||
|
} catch (error) {
|
||||||
|
this.errorText = (error && error.message) || '开放项目加载失败'
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
if (fromPullDown) {
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
continueEdit(item) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: item.route
|
||||||
|
})
|
||||||
|
},
|
||||||
|
copyText(value) {
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: String(value || ''),
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '已复制',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goDeclareList() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/gxmu/index'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goApply(item) {
|
||||||
|
if (!item.module || !getFormConfig(item.module)) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '当前申报项目在小程序端暂未配置',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const moduleMeta = getModuleMeta(item.module)
|
||||||
|
const title = String(item.title || '').trim() || ((moduleMeta && moduleMeta.title) || '申报表')
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/gxmu/form?code=${encodeURIComponent(item.module)}&year=${encodeURIComponent(item.year || '')}&declareTitle=${encodeURIComponent(title)}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top center, rgba(20, 150, 242, 0.1), transparent 30%),
|
||||||
|
linear-gradient(180deg, #f5fbff 0%, #eef7ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-scroll {
|
||||||
|
height: 100vh;
|
||||||
|
padding: 24rpx 24rpx 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card,
|
||||||
|
.section-card,
|
||||||
|
.data-card {
|
||||||
|
border-radius: 30rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
border: 1rpx solid rgba(20, 150, 242, 0.08);
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(34, 94, 142, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
padding: 32rpx 28rpx;
|
||||||
|
background: linear-gradient(145deg, #0d6fba 0%, #1496f2 58%, #5bc2ff 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-desc {
|
||||||
|
margin-top: 14rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: rgba(255, 255, 255, 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
padding: 18rpx 16rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-action {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #1496f2;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-block {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
padding: 26rpx 22rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #faf5f2;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.75;
|
||||||
|
color: #7b726d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-block--error {
|
||||||
|
color: #0f7dd1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-card {
|
||||||
|
padding: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #22272f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #8b817c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-row {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
padding: 18rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: #faf7f2;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.75;
|
||||||
|
color: #5f564f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
padding: 10rpx 16rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--success {
|
||||||
|
background: #eef9f1;
|
||||||
|
color: #1f8f49;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--warning {
|
||||||
|
background: #fff8e6;
|
||||||
|
color: #ad7b00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--default {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #8b817c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-action {
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(20, 150, 242, 0.08);
|
||||||
|
color: #1496f2;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
501
pages/mine/index.vue
Normal file
@@ -0,0 +1,501 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<scroll-view scroll-y class="page-scroll">
|
||||||
|
<common-hero title="我的">
|
||||||
|
|
||||||
|
<view class="hero-content">
|
||||||
|
<view class="profile-card">
|
||||||
|
<view class="profile-top">
|
||||||
|
<image v-if="profile.avatar" class="avatar-image" :src="profile.avatar" mode="aspectFill" />
|
||||||
|
<view v-else class="avatar">{{ avatarText }}</view>
|
||||||
|
<view class="profile-main">
|
||||||
|
<view class="profile-name">{{ displayName }}</view>
|
||||||
|
<view class="profile-role">{{ profileRole }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="profile-stats">
|
||||||
|
<view v-for="item in stats" :key="item.label" class="profile-stat">
|
||||||
|
<view class="profile-stat-value">{{ item.value }}</view>
|
||||||
|
<view class="profile-stat-label">{{ item.label }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="logout-btn" @click="handleLogout">退出登录</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</common-hero>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">常用入口</view>
|
||||||
|
<view class="entry-grid">
|
||||||
|
<view
|
||||||
|
v-for="item in visibleEntries"
|
||||||
|
:key="item.title"
|
||||||
|
class="entry-card"
|
||||||
|
@click="openEntry(item)"
|
||||||
|
>
|
||||||
|
<view class="entry-icon">{{ item.icon }}</view>
|
||||||
|
<view class="entry-title">{{ item.title }}</view>
|
||||||
|
<view class="entry-desc">{{ item.desc }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">最近状态</view>
|
||||||
|
<view class="status-list">
|
||||||
|
<view v-for="item in statusList" :key="item.title" class="status-card" @click="openStatus(item)">
|
||||||
|
<view class="status-main">
|
||||||
|
<view class="status-name">{{ item.title }}</view>
|
||||||
|
<view class="status-desc">{{ item.desc }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="status-tag">{{ item.tag }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CommonHero from '../../components/common-hero/common-hero.vue'
|
||||||
|
import {
|
||||||
|
ensureLoggedIn,
|
||||||
|
getCurrentUser,
|
||||||
|
getLocalProfileOverride,
|
||||||
|
getLocalDeclareRecords,
|
||||||
|
getManuscriptStatusMeta,
|
||||||
|
getReviewStatusMeta,
|
||||||
|
getUserProfile,
|
||||||
|
listManuscript,
|
||||||
|
logout,
|
||||||
|
userPageQmgcForm
|
||||||
|
} from './service'
|
||||||
|
import { hasReviewListAuthority } from '../../utils/gxmu/review-list-service'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CommonHero
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
profile: {
|
||||||
|
nickname: '',
|
||||||
|
realName: '',
|
||||||
|
username: '',
|
||||||
|
avatar: '',
|
||||||
|
organizationName: '',
|
||||||
|
tenantName: '',
|
||||||
|
merchantName: '',
|
||||||
|
roles: []
|
||||||
|
},
|
||||||
|
manuscriptList: [],
|
||||||
|
qingmaList: [],
|
||||||
|
declareList: [],
|
||||||
|
hasReviewListPermission: false,
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
title: '用户资料',
|
||||||
|
desc: '查看账号信息、组织身份和联系方式。',
|
||||||
|
icon: '资',
|
||||||
|
route: '/packageMine/profile'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '我的“青马”',
|
||||||
|
desc: '管理学员培养计划、考核与成长档案。',
|
||||||
|
icon: '青',
|
||||||
|
route: '/packageMine/qingma'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '我的稿件',
|
||||||
|
desc: '汇总我创建或参与编辑的全部稿件。',
|
||||||
|
icon: '稿',
|
||||||
|
route: '/packageMine/manuscripts'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '我的申报',
|
||||||
|
desc: '查看项目进度、待补材料与审核状态。',
|
||||||
|
icon: '报',
|
||||||
|
route: '/packageMine/declare'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '审核列表',
|
||||||
|
desc: '聚合查看业务数据、审核流并处理待审事项。',
|
||||||
|
icon: '审',
|
||||||
|
route: '/pages/gxmu/review-list',
|
||||||
|
authority: 'gxmu:reviewList:list'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayName() {
|
||||||
|
return this.profile.nickname || this.profile.realName || this.profile.username || '未登录用户'
|
||||||
|
},
|
||||||
|
avatarText() {
|
||||||
|
return (this.displayName || '我').slice(0, 2)
|
||||||
|
},
|
||||||
|
profileRole() {
|
||||||
|
const roles = Array.isArray(this.profile.roles) ? this.profile.roles : []
|
||||||
|
if (roles.length) {
|
||||||
|
return roles.map((item) => item.roleName).filter(Boolean).join(' / ')
|
||||||
|
}
|
||||||
|
return '校级团委数字工作台'
|
||||||
|
},
|
||||||
|
profileOrg() {
|
||||||
|
return this.profile.organizationName || this.profile.merchantName || this.profile.tenantName || '暂无组织信息'
|
||||||
|
},
|
||||||
|
stats() {
|
||||||
|
return [
|
||||||
|
{ value: `${this.manuscriptList.length}`, label: '我的稿件' },
|
||||||
|
{ value: `${this.declareList.length}`, label: '我的申报' },
|
||||||
|
{ value: `${this.qingmaList.length}`, label: '我的“青马”' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
visibleEntries() {
|
||||||
|
return this.entries.filter((item) => {
|
||||||
|
if (item.authority === 'gxmu:reviewList:list') {
|
||||||
|
return this.hasReviewListPermission
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
statusList() {
|
||||||
|
const manuscript = this.manuscriptList[0]
|
||||||
|
const qingma = this.qingmaList[0]
|
||||||
|
const declare = this.declareList[0]
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: '我的申报',
|
||||||
|
desc: declare ? `${declare.title}${declare.year ? ` · ${declare.year}` : ''},上次保存于 ${declare.savedAt || '刚刚'}` : '当前暂无本地申报记录,可前往五四评优页开始填写。',
|
||||||
|
tag: declare ? declare.status : '待创建',
|
||||||
|
route: '/packageMine/declare'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '我的“青马”',
|
||||||
|
desc: qingma ? `${qingma.name || '当前学员'} · ${qingma.schoolInfo || '暂无院系信息'}` : '当前暂无青马记录,可前往申报页查看开放项目。',
|
||||||
|
tag: qingma ? getReviewStatusMeta(qingma.reviewList).text : '待处理',
|
||||||
|
route: '/packageMine/qingma'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '我的稿件',
|
||||||
|
desc: manuscript ? `${manuscript.title || '未命名稿件'} · 最近更新时间 ${manuscript.updateTime || manuscript.createTime || '-'}` : '当前暂无稿件记录,可前往文稿工作台开始创作。',
|
||||||
|
tag: manuscript ? getManuscriptStatusMeta(manuscript).text : '待更新',
|
||||||
|
route: '/packageMine/manuscripts'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
if (!ensureLoggedIn({ redirectUrl: '/pages/mine/index' })) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.loadOverview()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showToast(title) {
|
||||||
|
uni.showToast({
|
||||||
|
title,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async loadOverview() {
|
||||||
|
const currentUser = getCurrentUser()
|
||||||
|
this.hasReviewListPermission = false
|
||||||
|
this.profile = {
|
||||||
|
...this.profile,
|
||||||
|
...currentUser
|
||||||
|
}
|
||||||
|
const localOverride = getLocalProfileOverride(currentUser.userId)
|
||||||
|
if (localOverride.nickname !== undefined) {
|
||||||
|
this.profile.nickname = String(localOverride.nickname || '').trim()
|
||||||
|
}
|
||||||
|
if (localOverride.realName !== undefined) {
|
||||||
|
this.profile.realName = String(localOverride.realName || '').trim()
|
||||||
|
}
|
||||||
|
if (localOverride.sex !== undefined || localOverride.sexName !== undefined) {
|
||||||
|
const nextGender = String(localOverride.sexName || localOverride.sex || '').trim()
|
||||||
|
this.profile.sex = nextGender
|
||||||
|
this.profile.sexName = nextGender
|
||||||
|
}
|
||||||
|
this.declareList = getLocalDeclareRecords()
|
||||||
|
try {
|
||||||
|
const [profile, manuscripts, qingmaResult] = await Promise.all([
|
||||||
|
getUserProfile().catch(() => null),
|
||||||
|
listManuscript().catch(() => []),
|
||||||
|
userPageQmgcForm({ page: 1, limit: 5 }).catch(() => ({ list: [] }))
|
||||||
|
])
|
||||||
|
if (profile) {
|
||||||
|
this.profile = {
|
||||||
|
...this.profile,
|
||||||
|
...profile
|
||||||
|
}
|
||||||
|
this.hasReviewListPermission = hasReviewListAuthority(profile)
|
||||||
|
const latestOverride = getLocalProfileOverride(this.profile.userId || currentUser.userId)
|
||||||
|
if (latestOverride.nickname !== undefined) {
|
||||||
|
this.profile.nickname = String(latestOverride.nickname || '').trim()
|
||||||
|
}
|
||||||
|
if (latestOverride.realName !== undefined) {
|
||||||
|
this.profile.realName = String(latestOverride.realName || '').trim()
|
||||||
|
}
|
||||||
|
if (latestOverride.sex !== undefined || latestOverride.sexName !== undefined) {
|
||||||
|
const nextGender = String(latestOverride.sexName || latestOverride.sex || '').trim()
|
||||||
|
this.profile.sex = nextGender
|
||||||
|
this.profile.sexName = nextGender
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const userId = Number(currentUser.userId || this.profile.userId || 0)
|
||||||
|
const list = Array.isArray(manuscripts) ? manuscripts : []
|
||||||
|
this.manuscriptList = userId ? list.filter((item) => Number(item.userId) === userId) : list
|
||||||
|
this.qingmaList = (qingmaResult && qingmaResult.list) || []
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('mine overview load failed', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleLogout() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确认退出当前登录账号?',
|
||||||
|
success: ({ confirm }) => {
|
||||||
|
if (!confirm) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logout()
|
||||||
|
this.showToast('已退出登录')
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.reLaunch({
|
||||||
|
url: '/pages/login/index?redirect=%2Fpages%2Fmine%2Findex'
|
||||||
|
})
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
openEntry(item) {
|
||||||
|
if (item.route) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: item.route
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showToast({
|
||||||
|
title: `${item.title}待接入`,
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
openStatus(item) {
|
||||||
|
if (item.route) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: item.route
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(180deg, #f8fbff 0%, #edf3f8 32%, #f7f7f8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-scroll {
|
||||||
|
height: 100vh;
|
||||||
|
padding-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
padding: 20rpx 24rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-card {
|
||||||
|
padding: 34rpx 30rpx;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
background: linear-gradient(145deg, #0d6fba 0%, #1496f2 58%, #5bc2ff 100%);
|
||||||
|
box-shadow: 0 18rpx 40rpx rgba(20, 118, 194, 0.18);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar,
|
||||||
|
.avatar-image {
|
||||||
|
width: 110rpx;
|
||||||
|
height: 110rpx;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-main {
|
||||||
|
margin-left: 22rpx;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-name {
|
||||||
|
font-size: 38rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-role {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.84);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-org {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
margin-top: 22rpx;
|
||||||
|
height: 76rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: rgba(11, 34, 58, 0.18);
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 76rpx;
|
||||||
|
text-align: center;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-stat {
|
||||||
|
padding: 20rpx 16rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-stat-value {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-stat-label {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-top: 30rpx;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
padding: 0 6rpx;
|
||||||
|
margin-bottom: 18rpx;
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-card,
|
||||||
|
.status-card {
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
border: 1rpx solid rgba(20, 150, 242, 0.08);
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(34, 94, 142, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-card {
|
||||||
|
padding: 24rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: rgba(20, 150, 242, 0.1);
|
||||||
|
color: #1496f2;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-title {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #262a31;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-desc {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #7a706b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-main {
|
||||||
|
flex: 1;
|
||||||
|
padding-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-name {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #23272f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-desc {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #7c736e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
padding: 12rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #eaf5ff;
|
||||||
|
color: #1496f2;
|
||||||
|
font-size: 22rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
422
pages/mine/manuscripts.vue
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<scroll-view scroll-y class="page-scroll">
|
||||||
|
<view class="hero-card">
|
||||||
|
<view class="hero-title">我的稿件</view>
|
||||||
|
<view class="hero-desc">汇总当前账号创建或参与的稿件记录,可查看状态和内容摘要。</view>
|
||||||
|
<view class="hero-stats">
|
||||||
|
<view class="stat-item">
|
||||||
|
<view class="stat-value">{{ manuscripts.length }}</view>
|
||||||
|
<view class="stat-label">稿件总数</view>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<view class="stat-value">{{ publishedCount }}</view>
|
||||||
|
<view class="stat-label">已通过/发布</view>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<view class="stat-value">{{ pendingCount }}</view>
|
||||||
|
<view class="stat-label">待处理</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="toolbar-card">
|
||||||
|
<uv-input
|
||||||
|
v-model="keyword"
|
||||||
|
class="search-input"
|
||||||
|
placeholder="搜索稿件标题或内容"
|
||||||
|
placeholder-style="color: #c0c4cc;"
|
||||||
|
:maxlength="-1"
|
||||||
|
/>
|
||||||
|
<view class="toolbar-actions">
|
||||||
|
<view class="toolbar-btn" @tap="loadData()">刷新</view>
|
||||||
|
<view class="toolbar-btn toolbar-btn--primary" @tap="goCreate">去创作</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading" class="state-card">
|
||||||
|
<view class="state-title">正在加载稿件...</view>
|
||||||
|
<view class="state-desc">请稍候,正在同步当前账号的稿件记录。</view>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="errorText" class="state-card state-card--error">
|
||||||
|
<view class="state-title">加载失败</view>
|
||||||
|
<view class="state-desc">{{ errorText }}</view>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="!filteredList.length" class="state-card">
|
||||||
|
<view class="state-title">暂无稿件</view>
|
||||||
|
<view class="state-desc">当前账号还没有可展示的稿件,可前往文稿工作台开始生成。</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="card-list">
|
||||||
|
<view v-for="item in filteredList" :key="item.id || item.title" class="data-card">
|
||||||
|
<view class="card-top">
|
||||||
|
<view class="card-main">
|
||||||
|
<view class="card-title">{{ item.title || '未命名稿件' }}</view>
|
||||||
|
<view class="card-subtitle">
|
||||||
|
创建时间:{{ formatDateTime(item.createTime) }} · 更新时间:{{ formatDateTime(item.updateTime) }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="status-tag" :class="`status-tag--${getStatusMeta(item).tone}`">
|
||||||
|
{{ getStatusMeta(item).text }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="item.cover" class="cover-wrap">
|
||||||
|
<image class="cover-image" :src="item.cover" mode="aspectFill" />
|
||||||
|
</view>
|
||||||
|
<view class="content-preview">{{ getContentPreview(item.content) }}</view>
|
||||||
|
<view class="card-actions">
|
||||||
|
<view class="card-action" @tap="toggleExpand(item)">{{ expandedId === item.id ? '收起内容' : '查看内容' }}</view>
|
||||||
|
<view class="card-action" @tap="copyTitle(item.title)">复制标题</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="expandedId === item.id" class="content-detail">{{ item.content || '暂无正文内容' }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
formatDateTime,
|
||||||
|
getCurrentUser,
|
||||||
|
getManuscriptStatusMeta,
|
||||||
|
listManuscript
|
||||||
|
} from './service'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
errorText: '',
|
||||||
|
keyword: '',
|
||||||
|
expandedId: null,
|
||||||
|
manuscripts: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filteredList() {
|
||||||
|
const keyword = String(this.keyword || '').trim().toLowerCase()
|
||||||
|
if (!keyword) {
|
||||||
|
return this.manuscripts
|
||||||
|
}
|
||||||
|
return this.manuscripts.filter((item) => {
|
||||||
|
return (
|
||||||
|
String(item.title || '').toLowerCase().includes(keyword) ||
|
||||||
|
String(item.content || '').toLowerCase().includes(keyword)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
publishedCount() {
|
||||||
|
return this.manuscripts.filter((item) => {
|
||||||
|
const text = this.getStatusMeta(item).text
|
||||||
|
return text === '通过' || text === '已发布'
|
||||||
|
}).length
|
||||||
|
},
|
||||||
|
pendingCount() {
|
||||||
|
return this.manuscripts.length - this.publishedCount
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
this.loadData()
|
||||||
|
},
|
||||||
|
onPullDownRefresh() {
|
||||||
|
this.loadData(true)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatDateTime,
|
||||||
|
getStatusMeta(item) {
|
||||||
|
return getManuscriptStatusMeta(item)
|
||||||
|
},
|
||||||
|
getContentPreview(content) {
|
||||||
|
const text = String(content || '').replace(/\s+/g, ' ').trim()
|
||||||
|
return text ? `${text.slice(0, 90)}${text.length > 90 ? '...' : ''}` : '暂无内容摘要'
|
||||||
|
},
|
||||||
|
async loadData(fromPullDown = false) {
|
||||||
|
this.loading = true
|
||||||
|
this.errorText = ''
|
||||||
|
try {
|
||||||
|
const user = getCurrentUser()
|
||||||
|
const result = await listManuscript()
|
||||||
|
const list = Array.isArray(result) ? result : []
|
||||||
|
this.manuscripts = list
|
||||||
|
.filter((item) => !user.userId || Number(item.userId) === Number(user.userId))
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftTime = new Date(String((left.updateTime || left.createTime || '')).replace(/-/g, '/')).getTime()
|
||||||
|
const rightTime = new Date(String((right.updateTime || right.createTime || '')).replace(/-/g, '/')).getTime()
|
||||||
|
return (Number.isNaN(rightTime) ? 0 : rightTime) - (Number.isNaN(leftTime) ? 0 : leftTime)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
this.errorText = (error && error.message) || '稿件加载失败'
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
if (fromPullDown) {
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleExpand(item) {
|
||||||
|
this.expandedId = this.expandedId === item.id ? null : item.id
|
||||||
|
},
|
||||||
|
copyTitle(title) {
|
||||||
|
if (!title) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '暂无标题可复制',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: title,
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '标题已复制',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goCreate() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/manuscript/index'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top center, rgba(240, 122, 61, 0.1), transparent 30%),
|
||||||
|
linear-gradient(180deg, #fff8f2 0%, #f8f2ec 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-scroll {
|
||||||
|
height: 100vh;
|
||||||
|
padding: 24rpx 24rpx 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card,
|
||||||
|
.toolbar-card,
|
||||||
|
.state-card,
|
||||||
|
.data-card {
|
||||||
|
border-radius: 30rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
border: 1rpx solid rgba(240, 122, 61, 0.08);
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(76, 49, 35, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
padding: 32rpx 28rpx;
|
||||||
|
background: linear-gradient(145deg, #7c2f04 0%, #c8641f 56%, #f29a4b 100%);
|
||||||
|
color: #fffaf5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-desc {
|
||||||
|
margin-top: 14rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: rgba(255, 250, 245, 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
padding: 18rpx 16rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: rgba(255, 250, 245, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-card {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.search-input.uv-input) {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 84rpx;
|
||||||
|
padding: 0 22rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
background: #fff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.search-input.uv-input .uv-input__content__field-wrapper__field) {
|
||||||
|
height: 84rpx;
|
||||||
|
min-height: 84rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 84rpx;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn {
|
||||||
|
height: 84rpx;
|
||||||
|
line-height: 84rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: #edf6ff;
|
||||||
|
color: #0f7dd1;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn--primary {
|
||||||
|
background: linear-gradient(135deg, #0f7dd1 0%, #1496f2 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding: 28rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card--error {
|
||||||
|
border-color: rgba(20, 150, 242, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #20252c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-desc {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.75;
|
||||||
|
color: #786e69;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-card {
|
||||||
|
padding: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #22272f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #8b817c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
padding: 10rpx 16rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--success {
|
||||||
|
background: #eef9f1;
|
||||||
|
color: #1f8f49;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--danger {
|
||||||
|
background: #fff1f0;
|
||||||
|
color: #d4380d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--warning {
|
||||||
|
background: #fff8e6;
|
||||||
|
color: #ad7b00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--default {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #8b817c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-wrap {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 260rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-preview,
|
||||||
|
.content-detail {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #5d544f;
|
||||||
|
word-break: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-detail {
|
||||||
|
padding: 20rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: #fbf7f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-action {
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(240, 122, 61, 0.08);
|
||||||
|
color: #c8641f;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
566
pages/mine/profile.vue
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<scroll-view scroll-y class="page-scroll">
|
||||||
|
<view class="section-card">
|
||||||
|
<view class="section-title">基础信息</view>
|
||||||
|
<view class="info-list">
|
||||||
|
<!-- <view class="info-item info-item--form">-->
|
||||||
|
<!-- <view class="info-main">-->
|
||||||
|
<!-- <view class="info-label">登录账号</view>-->
|
||||||
|
<!-- <view class="info-value">{{ profile.username || '-' }}</view>-->
|
||||||
|
<!-- </view>-->
|
||||||
|
<!-- </view>-->
|
||||||
|
<view class="info-item info-item--form">
|
||||||
|
<view class="info-main">
|
||||||
|
<view class="info-label">姓名</view>
|
||||||
|
<uv-input
|
||||||
|
class="info-input"
|
||||||
|
:value="editingRealName"
|
||||||
|
maxlength="20"
|
||||||
|
placeholder="请输入姓名"
|
||||||
|
placeholder-style="color: #b2a7a1;"
|
||||||
|
border="none"
|
||||||
|
@input="handleRealNameInput"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-item info-item--form">
|
||||||
|
<view class="info-main">
|
||||||
|
<view class="info-label">昵称</view>
|
||||||
|
<uv-input
|
||||||
|
class="info-input"
|
||||||
|
:value="editingNickname"
|
||||||
|
maxlength="20"
|
||||||
|
placeholder="请输入昵称"
|
||||||
|
placeholder-style="color: #b2a7a1;"
|
||||||
|
border="none"
|
||||||
|
@input="handleNicknameInput"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="info-action" @tap="fillWechatNickname">读取微信昵称</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-item info-item--form">
|
||||||
|
<view class="info-main">
|
||||||
|
<view class="info-label">性别</view>
|
||||||
|
<picker
|
||||||
|
mode="selector"
|
||||||
|
:range="genderOptions"
|
||||||
|
:value="genderIndex"
|
||||||
|
@change="handleGenderChange"
|
||||||
|
>
|
||||||
|
<view class="info-input info-input--picker">
|
||||||
|
{{ editingGender || '请选择性别' }}
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-item info-item--form">
|
||||||
|
<view class="info-main">
|
||||||
|
<view class="info-label">手机号码</view>
|
||||||
|
<view class="info-value">{{ profile.phone || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="profile.phone" class="info-action" @tap="copyText(profile.phone)">复制</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="info-actions">
|
||||||
|
<view class="nickname-btn nickname-btn--primary" @tap="saveProfileBasic">保存基础信息</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
getCurrentUser,
|
||||||
|
getLocalProfileOverride,
|
||||||
|
getUserProfile,
|
||||||
|
saveLocalProfileOverride,
|
||||||
|
updateUserProfileData
|
||||||
|
} from './service'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
editingNickname: '',
|
||||||
|
editingRealName: '',
|
||||||
|
editingGender: '',
|
||||||
|
genderOptions: ['男', '女', '未知'],
|
||||||
|
profile: {
|
||||||
|
userId: '',
|
||||||
|
nickname: '',
|
||||||
|
realName: '',
|
||||||
|
username: '',
|
||||||
|
avatar: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
organizationName: '',
|
||||||
|
tenantName: '',
|
||||||
|
merchantName: '',
|
||||||
|
sexName: '',
|
||||||
|
sex: '',
|
||||||
|
address: '',
|
||||||
|
province: '',
|
||||||
|
city: '',
|
||||||
|
region: '',
|
||||||
|
introduction: '',
|
||||||
|
roles: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayName() {
|
||||||
|
return this.profile.nickname || this.profile.realName || this.profile.username || '未命名用户'
|
||||||
|
},
|
||||||
|
roleText() {
|
||||||
|
const roles = Array.isArray(this.profile.roles) ? this.profile.roles : []
|
||||||
|
if (roles.length) {
|
||||||
|
return roles.map((item) => item.roleName).filter(Boolean).join(' / ')
|
||||||
|
}
|
||||||
|
return '暂无角色信息'
|
||||||
|
},
|
||||||
|
organizationText() {
|
||||||
|
return this.profile.organizationName || this.profile.merchantName || this.profile.tenantName || '暂无组织信息'
|
||||||
|
},
|
||||||
|
avatarText() {
|
||||||
|
return (this.displayName || '我').slice(0, 2)
|
||||||
|
},
|
||||||
|
genderIndex() {
|
||||||
|
const index = this.genderOptions.indexOf(this.editingGender)
|
||||||
|
return index > -1 ? index : 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
this.loadProfile()
|
||||||
|
},
|
||||||
|
onPullDownRefresh() {
|
||||||
|
this.loadProfile(true)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getProfileUserId() {
|
||||||
|
return Number(this.profile.userId || getCurrentUser().userId || 0) || ''
|
||||||
|
},
|
||||||
|
applyLocalProfileOverride() {
|
||||||
|
const override = getLocalProfileOverride(this.getProfileUserId())
|
||||||
|
if (override.nickname !== undefined) {
|
||||||
|
this.profile = {
|
||||||
|
...this.profile,
|
||||||
|
nickname: String(override.nickname || '').trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (override.realName !== undefined) {
|
||||||
|
this.profile = {
|
||||||
|
...this.profile,
|
||||||
|
realName: String(override.realName || '').trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (override.sex !== undefined || override.sexName !== undefined) {
|
||||||
|
const nextGender = String(override.sexName || override.sex || '').trim()
|
||||||
|
this.profile = {
|
||||||
|
...this.profile,
|
||||||
|
sex: nextGender,
|
||||||
|
sexName: nextGender
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.editingNickname = this.profile.nickname || ''
|
||||||
|
this.editingRealName = this.profile.realName || ''
|
||||||
|
this.editingGender = this.profile.sexName || this.profile.sex || ''
|
||||||
|
},
|
||||||
|
async loadProfile(fromPullDown = false) {
|
||||||
|
try {
|
||||||
|
const tokenUser = getCurrentUser()
|
||||||
|
this.profile = {
|
||||||
|
...this.profile,
|
||||||
|
...tokenUser
|
||||||
|
}
|
||||||
|
const result = await getUserProfile()
|
||||||
|
this.profile = {
|
||||||
|
...this.profile,
|
||||||
|
...(result || {})
|
||||||
|
}
|
||||||
|
this.applyLocalProfileOverride()
|
||||||
|
} catch (error) {
|
||||||
|
this.applyLocalProfileOverride()
|
||||||
|
if (fromPullDown) {
|
||||||
|
uni.showToast({
|
||||||
|
title: (error && error.message) || '用户资料加载失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (fromPullDown) {
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleNicknameInput(event) {
|
||||||
|
const value = typeof event === 'string' ? event : (event.detail && event.detail.value) || ''
|
||||||
|
this.editingNickname = String(value || '')
|
||||||
|
},
|
||||||
|
handleRealNameInput(event) {
|
||||||
|
const value = typeof event === 'string' ? event : (event.detail && event.detail.value) || ''
|
||||||
|
this.editingRealName = String(value || '')
|
||||||
|
},
|
||||||
|
handleGenderChange(event) {
|
||||||
|
const index = Number((event.detail && event.detail.value) || 0)
|
||||||
|
this.editingGender = this.genderOptions[index] || ''
|
||||||
|
},
|
||||||
|
async saveProfileBasic() {
|
||||||
|
const nickname = String(this.editingNickname || '').trim()
|
||||||
|
const realName = String(this.editingRealName || '').trim()
|
||||||
|
const gender = String(this.editingGender || '').trim()
|
||||||
|
try {
|
||||||
|
await updateUserProfileData({
|
||||||
|
userId: this.getProfileUserId() || undefined,
|
||||||
|
nickname,
|
||||||
|
realName,
|
||||||
|
sex: gender,
|
||||||
|
sexName: gender
|
||||||
|
})
|
||||||
|
this.profile = {
|
||||||
|
...this.profile,
|
||||||
|
nickname,
|
||||||
|
realName,
|
||||||
|
sex: gender,
|
||||||
|
sexName: gender
|
||||||
|
}
|
||||||
|
this.editingNickname = nickname
|
||||||
|
this.editingRealName = realName
|
||||||
|
this.editingGender = gender
|
||||||
|
saveLocalProfileOverride(this.getProfileUserId(), {
|
||||||
|
nickname,
|
||||||
|
realName,
|
||||||
|
sex: gender,
|
||||||
|
sexName: gender
|
||||||
|
})
|
||||||
|
uni.showToast({
|
||||||
|
title: '资料已保存',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({
|
||||||
|
title: (error && error.message) || '保存失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fillWechatNickname() {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
const assignNickname = (userInfo) => {
|
||||||
|
const nickname = String((userInfo && (userInfo.nickName || userInfo.nickname)) || '').trim()
|
||||||
|
if (!nickname) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '未读取到微信昵称',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.editingNickname = nickname
|
||||||
|
uni.showToast({
|
||||||
|
title: '已读取微信昵称',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (typeof uni.getUserProfile === 'function') {
|
||||||
|
uni.getUserProfile({
|
||||||
|
desc: '用于完善用户昵称',
|
||||||
|
success: (res) => {
|
||||||
|
assignNickname(res && res.userInfo)
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '未授权读取微信昵称',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof uni.getUserInfo === 'function') {
|
||||||
|
uni.getUserInfo({
|
||||||
|
success: (res) => {
|
||||||
|
assignNickname(res && res.userInfo)
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '读取微信昵称失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showToast({
|
||||||
|
title: '当前环境不支持读取微信昵称',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
// #endif
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
uni.showToast({
|
||||||
|
title: '仅微信小程序支持读取微信昵称',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
},
|
||||||
|
copyText(value) {
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: String(value || ''),
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '已复制',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goPage(url) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top center, rgba(20, 150, 242, 0.1), transparent 28%),
|
||||||
|
linear-gradient(180deg, #f5fbff 0%, #eef7ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-scroll {
|
||||||
|
height: 100vh;
|
||||||
|
padding: 24rpx 24rpx 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card,
|
||||||
|
.section-card {
|
||||||
|
border-radius: 32rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
border: 1rpx solid rgba(20, 150, 242, 0.08);
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(34, 94, 142, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
padding: 34rpx 30rpx;
|
||||||
|
background: linear-gradient(145deg, #0d6fba 0%, #1496f2 58%, #5bc2ff 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-image,
|
||||||
|
.avatar-text {
|
||||||
|
width: 112rpx;
|
||||||
|
height: 112rpx;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
margin-left: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-name {
|
||||||
|
font-size: 38rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-role,
|
||||||
|
.hero-org {
|
||||||
|
margin-top: 10rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: rgba(255, 255, 255, 0.84);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
padding: 20rpx 16rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2329;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 20rpx 18rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #faf5f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #8b817c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 25rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #2b3037;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-action {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #1496f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item--form {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.info-input.uv-input) {
|
||||||
|
width: 100%;
|
||||||
|
height: 92rpx;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
padding: 0 24rpx !important;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #ffffff !important;
|
||||||
|
border: 1rpx solid #eadfd7 !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.info-input.uv-input .uv-input__content__field-wrapper__field) {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #2b3037;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-input--picker {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-actions {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-btn {
|
||||||
|
height: 80rpx;
|
||||||
|
line-height: 80rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-btn--ghost {
|
||||||
|
background: #eef6ff;
|
||||||
|
color: #1496f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-btn--primary {
|
||||||
|
background: linear-gradient(135deg, #0d6fba 0%, #1496f2 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-tip {
|
||||||
|
margin-top: 14rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #8b817c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bio-text {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #746a65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-card {
|
||||||
|
padding: 22rpx 18rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: #fbf6f1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: rgba(20, 150, 242, 0.1);
|
||||||
|
color: #1496f2;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-title {
|
||||||
|
margin-top: 14rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2b3037;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
344
pages/mine/qingma.vue
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
<template>
|
||||||
|
<view class="page">
|
||||||
|
<scroll-view scroll-y class="page-scroll">
|
||||||
|
<view class="hero-card">
|
||||||
|
<view class="hero-title">我的“青马”</view>
|
||||||
|
<view class="hero-desc">查看青马工程报名记录、培养信息与当前审核状态。</view>
|
||||||
|
<view class="hero-stats">
|
||||||
|
<view class="stat-item">
|
||||||
|
<view class="stat-value">{{ records.length }}</view>
|
||||||
|
<view class="stat-label">记录总数</view>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<view class="stat-value">{{ approvedCount }}</view>
|
||||||
|
<view class="stat-label">已通过</view>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<view class="stat-value">{{ pendingCount }}</view>
|
||||||
|
<view class="stat-label">待跟进</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loading" class="state-card">
|
||||||
|
<view class="state-title">正在加载青马数据...</view>
|
||||||
|
<view class="state-desc">请稍候,正在同步你的青马工程记录。</view>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="errorText" class="state-card state-card--error">
|
||||||
|
<view class="state-title">加载失败</view>
|
||||||
|
<view class="state-desc">{{ errorText }}</view>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="!records.length" class="state-card">
|
||||||
|
<view class="state-title">暂未查询到青马记录</view>
|
||||||
|
<view class="state-desc">当前账号还没有青马工程报名或培养数据,可前往申报页查看开放项目。</view>
|
||||||
|
<view class="state-action" @tap="goApplyList">查看申报列表</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="card-list">
|
||||||
|
<view v-for="item in records" :key="item.id || `${item.name}-${item.year}`" class="data-card">
|
||||||
|
<view class="card-top">
|
||||||
|
<view class="card-main">
|
||||||
|
<view class="card-title">{{ item.name || '未命名学员' }}</view>
|
||||||
|
<view class="card-subtitle">{{ item.year || '未设置年度' }} · 青马工程</view>
|
||||||
|
</view>
|
||||||
|
<view class="status-tag" :class="`status-tag--${getStatusMeta(item).tone}`">
|
||||||
|
{{ getStatusMeta(item).text }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-list">
|
||||||
|
<view class="meta-item">
|
||||||
|
<view class="meta-label">学校院系</view>
|
||||||
|
<view class="meta-value">{{ item.schoolInfo || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-item">
|
||||||
|
<view class="meta-label">手机号码</view>
|
||||||
|
<view class="meta-value">{{ item.phone || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-item">
|
||||||
|
<view class="meta-label">团学职务</view>
|
||||||
|
<view class="meta-value">{{ item.leaguePosition || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="meta-item">
|
||||||
|
<view class="meta-label">综合成绩</view>
|
||||||
|
<view class="meta-value">{{ item.academicPerformance || '-' }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="card-actions">
|
||||||
|
<view class="card-action" @tap="copyValue(item.phone)">复制电话</view>
|
||||||
|
<view class="card-action" @tap="copyValue(item.schoolInfo)">复制院系</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getReviewStatusMeta, userPageQmgcForm } from './service'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
errorText: '',
|
||||||
|
records: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
approvedCount() {
|
||||||
|
return this.records.filter((item) => this.getStatusMeta(item).text === '通过').length
|
||||||
|
},
|
||||||
|
pendingCount() {
|
||||||
|
return this.records.filter((item) => this.getStatusMeta(item).text !== '通过').length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
this.loadData()
|
||||||
|
},
|
||||||
|
onPullDownRefresh() {
|
||||||
|
this.loadData(true)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getStatusMeta(item) {
|
||||||
|
return getReviewStatusMeta(item && item.reviewList)
|
||||||
|
},
|
||||||
|
async loadData(fromPullDown = false) {
|
||||||
|
this.loading = true
|
||||||
|
this.errorText = ''
|
||||||
|
try {
|
||||||
|
const result = await userPageQmgcForm({
|
||||||
|
page: 1,
|
||||||
|
limit: 20
|
||||||
|
})
|
||||||
|
this.records = (result && result.list) || []
|
||||||
|
} catch (error) {
|
||||||
|
this.errorText = (error && error.message) || '青马数据加载失败'
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
if (fromPullDown) {
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
copyValue(value) {
|
||||||
|
if (!value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '暂无可复制内容',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: String(value),
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '已复制',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goApplyList() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/gxmu/index'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top center, rgba(184, 146, 33, 0.1), transparent 30%),
|
||||||
|
linear-gradient(180deg, #f8f4eb 0%, #f4efe5 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-scroll {
|
||||||
|
height: 100vh;
|
||||||
|
padding: 24rpx 24rpx 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card,
|
||||||
|
.state-card,
|
||||||
|
.data-card {
|
||||||
|
border-radius: 30rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
border: 1rpx solid rgba(184, 146, 33, 0.08);
|
||||||
|
box-shadow: 0 10rpx 28rpx rgba(76, 49, 35, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
padding: 32rpx 28rpx;
|
||||||
|
background: linear-gradient(145deg, #6f4f05 0%, #a37a11 56%, #d4b24a 100%);
|
||||||
|
color: #fffdf4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-desc {
|
||||||
|
margin-top: 14rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: rgba(255, 253, 244, 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
padding: 18rpx 16rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: rgba(255, 253, 244, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding: 28rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-card--error {
|
||||||
|
border-color: rgba(195, 49, 30, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #20252c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-desc {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.75;
|
||||||
|
color: #786e69;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-action {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #a37a11;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-card {
|
||||||
|
padding: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #22272f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #8b817c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
padding: 10rpx 16rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--success {
|
||||||
|
background: #eef9f1;
|
||||||
|
color: #1f8f49;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--danger {
|
||||||
|
background: #fff1f0;
|
||||||
|
color: #d4380d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--warning {
|
||||||
|
background: #fff8e6;
|
||||||
|
color: #ad7b00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag--default {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #8b817c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
padding: 18rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: #faf7f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-label {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #8b817c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-value {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #2d333b;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 14rpx;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-action {
|
||||||
|
padding: 10rpx 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: rgba(163, 122, 17, 0.08);
|
||||||
|
color: #a37a11;
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
187
pages/mine/service.js
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import {
|
||||||
|
API_BASE_URL,
|
||||||
|
apiRequest,
|
||||||
|
getCurrentUser
|
||||||
|
} from '../assistant/chat-service'
|
||||||
|
import { clearAuthStorage, ensureLoggedIn, get, hasToken } from '../../utils/request'
|
||||||
|
import { getModuleMeta } from '../../utils/gxmu/config'
|
||||||
|
|
||||||
|
const LOCAL_DECLARE_PREFIX = 'gxmu_form_draft_'
|
||||||
|
const LOCAL_PROFILE_OVERRIDE_PREFIX = 'mine_profile_override_'
|
||||||
|
|
||||||
|
export { getCurrentUser }
|
||||||
|
export { hasToken, ensureLoggedIn }
|
||||||
|
|
||||||
|
export function logout() {
|
||||||
|
clearAuthStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProfileOverrideStorageKey(userId) {
|
||||||
|
const normalizedUserId = String(userId || '').trim()
|
||||||
|
return `${LOCAL_PROFILE_OVERRIDE_PREFIX}${normalizedUserId || 'default'}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLocalProfileOverride(userId) {
|
||||||
|
try {
|
||||||
|
return uni.getStorageSync(getProfileOverrideStorageKey(userId)) || {}
|
||||||
|
} catch (error) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveLocalProfileOverride(userId, payload) {
|
||||||
|
const value = payload && typeof payload === 'object' ? payload : {}
|
||||||
|
uni.setStorageSync(getProfileOverrideStorageKey(userId), value)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserProfile() {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${API_BASE_URL}/auth/user`,
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateUserProfileData(data) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${API_BASE_URL}/system/user/update-data`,
|
||||||
|
method: 'POST',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listOrganizations(params) {
|
||||||
|
return get('/system/organization', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listManuscript(params) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${API_BASE_URL}/cms/manuscript`,
|
||||||
|
method: 'GET',
|
||||||
|
data: params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userPageQmgcForm(params) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${API_BASE_URL}/gxmu/qmgc-form/userPage`,
|
||||||
|
method: 'GET',
|
||||||
|
data: params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listAvailableDeclare(params) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${API_BASE_URL}/gxmu/declare`,
|
||||||
|
method: 'GET',
|
||||||
|
data: params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDateTime(value) {
|
||||||
|
if (!value) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
const date = new Date(String(value).replace(/-/g, '/'))
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
const pad = (item) => String(item).padStart(2, '0')
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLatestReview(reviewList) {
|
||||||
|
if (!Array.isArray(reviewList) || !reviewList.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return reviewList[reviewList.length - 1] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getReviewStatusMeta(reviewList) {
|
||||||
|
const latest = getLatestReview(reviewList)
|
||||||
|
if (!latest) {
|
||||||
|
return {
|
||||||
|
text: '未审核',
|
||||||
|
tone: 'default'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (latest.status === 1) {
|
||||||
|
return {
|
||||||
|
text: '通过',
|
||||||
|
tone: 'success'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (latest.status === 0 || latest.status === 2) {
|
||||||
|
return {
|
||||||
|
text: '不通过',
|
||||||
|
tone: 'danger'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
text: '待审核',
|
||||||
|
tone: 'warning'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getManuscriptStatusMeta(record) {
|
||||||
|
const reviewMeta = getReviewStatusMeta(record && record.reviewList)
|
||||||
|
if (reviewMeta.text !== '未审核') {
|
||||||
|
return reviewMeta
|
||||||
|
}
|
||||||
|
if (record && Number(record.status) === 1) {
|
||||||
|
return {
|
||||||
|
text: '已发布',
|
||||||
|
tone: 'success'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (record && Number(record.status) === 0) {
|
||||||
|
return {
|
||||||
|
text: '待审核',
|
||||||
|
tone: 'warning'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
text: '未审核',
|
||||||
|
tone: 'default'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLocalDeclareRecords() {
|
||||||
|
try {
|
||||||
|
const storageInfo = uni.getStorageInfoSync()
|
||||||
|
const keys = (storageInfo.keys || []).filter((key) => key.indexOf(LOCAL_DECLARE_PREFIX) === 0)
|
||||||
|
return keys
|
||||||
|
.map((key) => {
|
||||||
|
const draft = uni.getStorageSync(key)
|
||||||
|
if (!draft || !draft.formData) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const moduleCode = draft.moduleCode || ''
|
||||||
|
if (!moduleCode) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const moduleMeta = getModuleMeta(moduleCode)
|
||||||
|
const year = String((draft.formData && draft.formData.year) || '').trim()
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
moduleCode,
|
||||||
|
title: (moduleMeta && moduleMeta.title) || '未命名申报',
|
||||||
|
group: (moduleMeta && moduleMeta.group) || '申报项目',
|
||||||
|
icon: (moduleMeta && moduleMeta.icon) || '申',
|
||||||
|
year,
|
||||||
|
savedAt: draft.savedAt || '',
|
||||||
|
status: draft.status === 'submitted' ? '已提交' : '草稿',
|
||||||
|
formData: draft.formData,
|
||||||
|
route: `/pages/gxmu/form?code=${encodeURIComponent(moduleCode)}&year=${encodeURIComponent(year)}&declareTitle=${encodeURIComponent((moduleMeta && moduleMeta.title) || '申报表')}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftTime = new Date(String((left && left.savedAt) || '').replace(/-/g, '/')).getTime()
|
||||||
|
const rightTime = new Date(String((right && right.savedAt) || '').replace(/-/g, '/')).getTime()
|
||||||
|
return (Number.isNaN(rightTime) ? 0 : rightTime) - (Number.isNaN(leftTime) ? 0 : leftTime)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
1260
pages/tygl/index.vue
Normal file
47
pages/tygl/service.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { get } from '../../utils/request'
|
||||||
|
import { API_BASE_URL, apiRequest, getCurrentUser } from '../assistant/chat-service'
|
||||||
|
|
||||||
|
export { getCurrentUser }
|
||||||
|
|
||||||
|
export function listDictData(params) {
|
||||||
|
return get('/system/dict-data', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listOrganizations(params) {
|
||||||
|
return get('/system/organization', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listColleges(params) {
|
||||||
|
return get('/gxmu/college', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listClasses(params) {
|
||||||
|
return get('/gxmu/class', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserProfile() {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${API_BASE_URL}/auth/user`,
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentTyglForm() {
|
||||||
|
return get('/gxmu/tygl-form/current')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveTyglForm(data) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${API_BASE_URL}/gxmu/tygl-form`,
|
||||||
|
method: 'POST',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTyglForm(data) {
|
||||||
|
return apiRequest({
|
||||||
|
url: `${API_BASE_URL}/gxmu/tygl-form`,
|
||||||
|
method: 'PUT',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
BIN
static/indexBg.jpg
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
static/logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
static/swiper.jpg
Normal file
|
After Width: | Height: | Size: 354 KiB |
BIN
static/tabbar/chat-active.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
static/tabbar/chat.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
static/tabbar/home-active.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
static/tabbar/home.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
static/tabbar/user-active.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
static/tabbar/user.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
13
uni.promisify.adaptor.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
uni.addInterceptor({
|
||||||
|
returnValue (res) {
|
||||||
|
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
res.then((res) => {
|
||||||
|
if (!res) return resolve(res)
|
||||||
|
return res[0] ? reject(res[0]) : resolve(res[1])
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
78
uni.scss
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* 这里是uni-app内置的常用样式变量
|
||||||
|
*
|
||||||
|
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||||
|
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
@import "@/uni_modules/uv-ui-tools/theme.scss";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||||
|
*
|
||||||
|
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* 颜色变量 */
|
||||||
|
|
||||||
|
/* 行为相关颜色 */
|
||||||
|
$uni-color-primary: #1496f2;
|
||||||
|
$uni-color-success: #4cd964;
|
||||||
|
$uni-color-warning: #f0ad4e;
|
||||||
|
$uni-color-error: #dd524d;
|
||||||
|
|
||||||
|
/* 文字基本颜色 */
|
||||||
|
$uni-text-color:#333;//基本色
|
||||||
|
$uni-text-color-inverse:#fff;//反色
|
||||||
|
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
|
||||||
|
$uni-text-color-placeholder: #808080;
|
||||||
|
$uni-text-color-disable:#c0c0c0;
|
||||||
|
|
||||||
|
/* 背景颜色 */
|
||||||
|
$uni-bg-color:#ffffff;
|
||||||
|
$uni-bg-color-grey:#f8f8f8;
|
||||||
|
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
|
||||||
|
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
|
||||||
|
|
||||||
|
/* 边框颜色 */
|
||||||
|
$uni-border-color:#c8c7cc;
|
||||||
|
|
||||||
|
/* 尺寸变量 */
|
||||||
|
|
||||||
|
/* 文字尺寸 */
|
||||||
|
$uni-font-size-sm:12px;
|
||||||
|
$uni-font-size-base:14px;
|
||||||
|
$uni-font-size-lg:16px;
|
||||||
|
|
||||||
|
/* 图片尺寸 */
|
||||||
|
$uni-img-size-sm:20px;
|
||||||
|
$uni-img-size-base:26px;
|
||||||
|
$uni-img-size-lg:40px;
|
||||||
|
|
||||||
|
/* Border Radius */
|
||||||
|
$uni-border-radius-sm: 2px;
|
||||||
|
$uni-border-radius-base: 3px;
|
||||||
|
$uni-border-radius-lg: 6px;
|
||||||
|
$uni-border-radius-circle: 50%;
|
||||||
|
|
||||||
|
/* 水平间距 */
|
||||||
|
$uni-spacing-row-sm: 5px;
|
||||||
|
$uni-spacing-row-base: 10px;
|
||||||
|
$uni-spacing-row-lg: 15px;
|
||||||
|
|
||||||
|
/* 垂直间距 */
|
||||||
|
$uni-spacing-col-sm: 4px;
|
||||||
|
$uni-spacing-col-base: 8px;
|
||||||
|
$uni-spacing-col-lg: 12px;
|
||||||
|
|
||||||
|
/* 透明度 */
|
||||||
|
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||||
|
|
||||||
|
/* 文章场景相关 */
|
||||||
|
$uni-color-title: #2C405A; // 文章标题颜色
|
||||||
|
$uni-font-size-title:20px;
|
||||||
|
$uni-color-subtitle: #555555; // 二级标题颜色
|
||||||
|
$uni-font-size-subtitle:26px;
|
||||||
|
$uni-color-paragraph: #3F536E; // 文章段落颜色
|
||||||
|
$uni-font-size-paragraph:15px;
|
||||||
31
uni_modules/uv-icon/changelog.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
## 1.0.13(2023-12-06)
|
||||||
|
1. 优化
|
||||||
|
## 1.0.12(2023-12-06)
|
||||||
|
1. 阻止事件冒泡处理
|
||||||
|
## 1.0.11(2023-10-29)
|
||||||
|
1. imgMode默认值改成aspectFit
|
||||||
|
## 1.0.10(2023-08-13)
|
||||||
|
1. 优化nvue,方便自定义图标
|
||||||
|
## 1.0.9(2023-07-28)
|
||||||
|
1. 修改几个对应错误图标的BUG
|
||||||
|
## 1.0.8(2023-07-24)
|
||||||
|
1. 优化 支持base64图片
|
||||||
|
## 1.0.7(2023-07-17)
|
||||||
|
1. 修复 uv-icon 恢复uv-empty相关的图标
|
||||||
|
## 1.0.6(2023-07-13)
|
||||||
|
1. 修复icon设置name属性对应图标错误的BUG
|
||||||
|
## 1.0.5(2023-07-04)
|
||||||
|
1. 更新图标,删除一些不常用的图标
|
||||||
|
2. 删除base64,修改成ttf文件引入读取图标
|
||||||
|
3. 自定义图标文档说明:https://www.uvui.cn/guide/customIcon.html
|
||||||
|
## 1.0.4(2023-07-03)
|
||||||
|
1. 修复主题颜色在APP不生效的BUG
|
||||||
|
## 1.0.3(2023-05-24)
|
||||||
|
1. 将线上ttf字体包替换成base64,避免加载时或者网络差时候显示白色方块
|
||||||
|
## 1.0.2(2023-05-16)
|
||||||
|
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
|
||||||
|
2. 优化部分功能
|
||||||
|
## 1.0.1(2023-05-10)
|
||||||
|
1. 修复小程序中异常显示
|
||||||
|
## 1.0.0(2023-05-04)
|
||||||
|
新发版
|
||||||
160
uni_modules/uv-icon/components/uv-icon/icons.js
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
export default {
|
||||||
|
'uvicon-level': 'e68f',
|
||||||
|
'uvicon-checkbox-mark': 'e659',
|
||||||
|
'uvicon-folder': 'e694',
|
||||||
|
'uvicon-movie': 'e67c',
|
||||||
|
'uvicon-star-fill': 'e61e',
|
||||||
|
'uvicon-star': 'e618',
|
||||||
|
'uvicon-phone-fill': 'e6ac',
|
||||||
|
'uvicon-phone': 'e6ba',
|
||||||
|
'uvicon-apple-fill': 'e635',
|
||||||
|
'uvicon-backspace': 'e64d',
|
||||||
|
'uvicon-attach': 'e640',
|
||||||
|
'uvicon-empty-data': 'e671',
|
||||||
|
'uvicon-empty-address': 'e68a',
|
||||||
|
'uvicon-empty-favor': 'e662',
|
||||||
|
'uvicon-empty-car': 'e657',
|
||||||
|
'uvicon-empty-order': 'e66b',
|
||||||
|
'uvicon-empty-list': 'e672',
|
||||||
|
'uvicon-empty-search': 'e677',
|
||||||
|
'uvicon-empty-permission': 'e67d',
|
||||||
|
'uvicon-empty-news': 'e67e',
|
||||||
|
'uvicon-empty-history': 'e685',
|
||||||
|
'uvicon-empty-coupon': 'e69b',
|
||||||
|
'uvicon-empty-page': 'e60e',
|
||||||
|
'uvicon-empty-wifi-off': 'e6cc',
|
||||||
|
'uvicon-reload': 'e627',
|
||||||
|
'uvicon-order': 'e695',
|
||||||
|
'uvicon-server-man': 'e601',
|
||||||
|
'uvicon-search': 'e632',
|
||||||
|
'uvicon-more-dot-fill': 'e66f',
|
||||||
|
'uvicon-scan': 'e631',
|
||||||
|
'uvicon-map': 'e665',
|
||||||
|
'uvicon-map-fill': 'e6a8',
|
||||||
|
'uvicon-tags': 'e621',
|
||||||
|
'uvicon-tags-fill': 'e613',
|
||||||
|
'uvicon-eye': 'e664',
|
||||||
|
'uvicon-eye-fill': 'e697',
|
||||||
|
'uvicon-eye-off': 'e69c',
|
||||||
|
'uvicon-eye-off-outline': 'e688',
|
||||||
|
'uvicon-mic': 'e66d',
|
||||||
|
'uvicon-mic-off': 'e691',
|
||||||
|
'uvicon-calendar': 'e65c',
|
||||||
|
'uvicon-trash': 'e623',
|
||||||
|
'uvicon-trash-fill': 'e6ce',
|
||||||
|
'uvicon-play-left': 'e6bf',
|
||||||
|
'uvicon-play-right': 'e6b3',
|
||||||
|
'uvicon-minus': 'e614',
|
||||||
|
'uvicon-plus': 'e625',
|
||||||
|
'uvicon-info-circle': 'e69f',
|
||||||
|
'uvicon-info-circle-fill': 'e6a7',
|
||||||
|
'uvicon-question-circle': 'e622',
|
||||||
|
'uvicon-question-circle-fill': 'e6bc',
|
||||||
|
'uvicon-close': 'e65a',
|
||||||
|
'uvicon-checkmark': 'e64a',
|
||||||
|
'uvicon-checkmark-circle': 'e643',
|
||||||
|
'uvicon-checkmark-circle-fill': 'e668',
|
||||||
|
'uvicon-setting': 'e602',
|
||||||
|
'uvicon-setting-fill': 'e6d0',
|
||||||
|
'uvicon-heart': 'e6a2',
|
||||||
|
'uvicon-heart-fill': 'e68b',
|
||||||
|
'uvicon-camera': 'e642',
|
||||||
|
'uvicon-camera-fill': 'e650',
|
||||||
|
'uvicon-more-circle': 'e69e',
|
||||||
|
'uvicon-more-circle-fill': 'e684',
|
||||||
|
'uvicon-chat': 'e656',
|
||||||
|
'uvicon-chat-fill': 'e63f',
|
||||||
|
'uvicon-bag': 'e647',
|
||||||
|
'uvicon-error-circle': 'e66e',
|
||||||
|
'uvicon-error-circle-fill': 'e655',
|
||||||
|
'uvicon-close-circle': 'e64e',
|
||||||
|
'uvicon-close-circle-fill': 'e666',
|
||||||
|
'uvicon-share': 'e629',
|
||||||
|
'uvicon-share-fill': 'e6bb',
|
||||||
|
'uvicon-share-square': 'e6c4',
|
||||||
|
'uvicon-shopping-cart': 'e6cb',
|
||||||
|
'uvicon-shopping-cart-fill': 'e630',
|
||||||
|
'uvicon-bell': 'e651',
|
||||||
|
'uvicon-bell-fill': 'e604',
|
||||||
|
'uvicon-list': 'e690',
|
||||||
|
'uvicon-list-dot': 'e6a9',
|
||||||
|
'uvicon-zhifubao-circle-fill': 'e617',
|
||||||
|
'uvicon-weixin-circle-fill': 'e6cd',
|
||||||
|
'uvicon-weixin-fill': 'e620',
|
||||||
|
'uvicon-qq-fill': 'e608',
|
||||||
|
'uvicon-qq-circle-fill': 'e6b9',
|
||||||
|
'uvicon-moments-circel-fill': 'e6c2',
|
||||||
|
'uvicon-moments': 'e6a0',
|
||||||
|
'uvicon-car': 'e64f',
|
||||||
|
'uvicon-car-fill': 'e648',
|
||||||
|
'uvicon-warning-fill': 'e6c7',
|
||||||
|
'uvicon-warning': 'e6c1',
|
||||||
|
'uvicon-clock-fill': 'e64b',
|
||||||
|
'uvicon-clock': 'e66c',
|
||||||
|
'uvicon-edit-pen': 'e65d',
|
||||||
|
'uvicon-edit-pen-fill': 'e679',
|
||||||
|
'uvicon-email': 'e673',
|
||||||
|
'uvicon-email-fill': 'e683',
|
||||||
|
'uvicon-minus-circle': 'e6a5',
|
||||||
|
'uvicon-plus-circle': 'e603',
|
||||||
|
'uvicon-plus-circle-fill': 'e611',
|
||||||
|
'uvicon-file-text': 'e687',
|
||||||
|
'uvicon-file-text-fill': 'e67f',
|
||||||
|
'uvicon-pushpin': 'e6d1',
|
||||||
|
'uvicon-pushpin-fill': 'e6b6',
|
||||||
|
'uvicon-grid': 'e68c',
|
||||||
|
'uvicon-grid-fill': 'e698',
|
||||||
|
'uvicon-play-circle': 'e6af',
|
||||||
|
'uvicon-play-circle-fill': 'e62a',
|
||||||
|
'uvicon-pause-circle-fill': 'e60c',
|
||||||
|
'uvicon-pause': 'e61c',
|
||||||
|
'uvicon-pause-circle': 'e696',
|
||||||
|
'uvicon-gift-fill': 'e6b0',
|
||||||
|
'uvicon-gift': 'e680',
|
||||||
|
'uvicon-kefu-ermai': 'e660',
|
||||||
|
'uvicon-server-fill': 'e610',
|
||||||
|
'uvicon-coupon-fill': 'e64c',
|
||||||
|
'uvicon-coupon': 'e65f',
|
||||||
|
'uvicon-integral': 'e693',
|
||||||
|
'uvicon-integral-fill': 'e6b1',
|
||||||
|
'uvicon-home-fill': 'e68e',
|
||||||
|
'uvicon-home': 'e67b',
|
||||||
|
'uvicon-account': 'e63a',
|
||||||
|
'uvicon-account-fill': 'e653',
|
||||||
|
'uvicon-thumb-down-fill': 'e628',
|
||||||
|
'uvicon-thumb-down': 'e60a',
|
||||||
|
'uvicon-thumb-up': 'e612',
|
||||||
|
'uvicon-thumb-up-fill': 'e62c',
|
||||||
|
'uvicon-lock-fill': 'e6a6',
|
||||||
|
'uvicon-lock-open': 'e68d',
|
||||||
|
'uvicon-lock-opened-fill': 'e6a1',
|
||||||
|
'uvicon-lock': 'e69d',
|
||||||
|
'uvicon-red-packet': 'e6c3',
|
||||||
|
'uvicon-photo-fill': 'e6b4',
|
||||||
|
'uvicon-photo': 'e60d',
|
||||||
|
'uvicon-volume-off-fill': 'e6c8',
|
||||||
|
'uvicon-volume-off': 'e6bd',
|
||||||
|
'uvicon-volume-fill': 'e624',
|
||||||
|
'uvicon-volume': 'e605',
|
||||||
|
'uvicon-download': 'e670',
|
||||||
|
'uvicon-arrow-up-fill': 'e636',
|
||||||
|
'uvicon-arrow-down-fill': 'e638',
|
||||||
|
'uvicon-play-left-fill': 'e6ae',
|
||||||
|
'uvicon-play-right-fill': 'e6ad',
|
||||||
|
'uvicon-arrow-downward': 'e634',
|
||||||
|
'uvicon-arrow-leftward': 'e63b',
|
||||||
|
'uvicon-arrow-rightward': 'e644',
|
||||||
|
'uvicon-arrow-upward': 'e641',
|
||||||
|
'uvicon-arrow-down': 'e63e',
|
||||||
|
'uvicon-arrow-right': 'e63c',
|
||||||
|
'uvicon-arrow-left': 'e646',
|
||||||
|
'uvicon-arrow-up': 'e633',
|
||||||
|
'uvicon-skip-back-left': 'e6c5',
|
||||||
|
'uvicon-skip-forward-right': 'e61f',
|
||||||
|
'uvicon-arrow-left-double': 'e637',
|
||||||
|
'uvicon-man': 'e675',
|
||||||
|
'uvicon-woman': 'e626',
|
||||||
|
'uvicon-en': 'e6b8',
|
||||||
|
'uvicon-twitte': 'e607',
|
||||||
|
'uvicon-twitter-circle-fill': 'e6cf'
|
||||||
|
}
|
||||||
90
uni_modules/uv-icon/components/uv-icon/props.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
// 图标类名
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 图标颜色,可接受主题色
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: '#606266'
|
||||||
|
},
|
||||||
|
// 字体大小,单位px
|
||||||
|
size: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '16px'
|
||||||
|
},
|
||||||
|
// 是否显示粗体
|
||||||
|
bold: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 点击图标的时候传递事件出去的index(用于区分点击了哪一个)
|
||||||
|
index: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
// 触摸图标时的类名
|
||||||
|
hoverClass: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 自定义扩展前缀,方便用户扩展自己的图标库
|
||||||
|
customPrefix: {
|
||||||
|
type: String,
|
||||||
|
default: 'uvicon'
|
||||||
|
},
|
||||||
|
// 图标右边或者下面的文字
|
||||||
|
label: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// label的位置,只能右边或者下边
|
||||||
|
labelPos: {
|
||||||
|
type: String,
|
||||||
|
default: 'right'
|
||||||
|
},
|
||||||
|
// label的大小
|
||||||
|
labelSize: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '15px'
|
||||||
|
},
|
||||||
|
// label的颜色
|
||||||
|
labelColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#606266'
|
||||||
|
},
|
||||||
|
// label与图标的距离
|
||||||
|
space: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '3px'
|
||||||
|
},
|
||||||
|
// 图片的mode
|
||||||
|
imgMode: {
|
||||||
|
type: String,
|
||||||
|
default: 'aspectFit'
|
||||||
|
},
|
||||||
|
// 用于显示图片小图标时,图片的宽度
|
||||||
|
width: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 用于显示图片小图标时,图片的高度
|
||||||
|
height: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 用于解决某些情况下,让图标垂直居中的用途
|
||||||
|
top: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
// 是否阻止事件传播
|
||||||
|
stop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
...uni.$uv?.props?.icon
|
||||||
|
}
|
||||||
|
}
|
||||||
226
uni_modules/uv-icon/components/uv-icon/uv-icon.vue
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
class="uv-icon"
|
||||||
|
@tap="clickHandler"
|
||||||
|
:class="['uv-icon--' + labelPos]"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="uv-icon__img"
|
||||||
|
v-if="isImg"
|
||||||
|
:src="name"
|
||||||
|
:mode="imgMode"
|
||||||
|
:style="[imgStyle, $uv.addStyle(customStyle)]"
|
||||||
|
></image>
|
||||||
|
<text
|
||||||
|
v-else
|
||||||
|
class="uv-icon__icon"
|
||||||
|
:class="uClasses"
|
||||||
|
:style="[iconStyle, $uv.addStyle(customStyle)]"
|
||||||
|
:hover-class="hoverClass"
|
||||||
|
>{{icon}}</text>
|
||||||
|
<!-- 这里进行空字符串判断,如果仅仅是v-if="label",可能会出现传递0的时候,结果也无法显示 -->
|
||||||
|
<text
|
||||||
|
v-if="label !== ''"
|
||||||
|
class="uv-icon__label"
|
||||||
|
:style="{
|
||||||
|
color: labelColor,
|
||||||
|
fontSize: $uv.addUnit(labelSize),
|
||||||
|
marginLeft: labelPos == 'right' ? $uv.addUnit(space) : 0,
|
||||||
|
marginTop: labelPos == 'bottom' ? $uv.addUnit(space) : 0,
|
||||||
|
marginRight: labelPos == 'left' ? $uv.addUnit(space) : 0,
|
||||||
|
marginBottom: labelPos == 'top' ? $uv.addUnit(space) : 0
|
||||||
|
}"
|
||||||
|
>{{ label }}</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
|
||||||
|
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
|
||||||
|
// #ifdef APP-NVUE
|
||||||
|
// nvue通过weex的dom模块引入字体,相关文档地址如下:
|
||||||
|
// https://weex.apache.org/zh/docs/modules/dom.html#addrule
|
||||||
|
import iconUrl from './uvicons.ttf';
|
||||||
|
const domModule = weex.requireModule('dom')
|
||||||
|
domModule.addRule('fontFace', {
|
||||||
|
'fontFamily': "uvicon-iconfont",
|
||||||
|
'src': "url('" + iconUrl + "')"
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
// 引入图标名称,已经对应的unicode
|
||||||
|
import icons from './icons';
|
||||||
|
import props from './props.js';
|
||||||
|
/**
|
||||||
|
* icon 图标
|
||||||
|
* @description 基于字体的图标集,包含了大多数常见场景的图标。
|
||||||
|
* @tutorial https://www.uvui.cn/components/icon.html
|
||||||
|
* @property {String} name 图标名称,见示例图标集
|
||||||
|
* @property {String} color 图标颜色,可接受主题色 (默认 color['uv-content-color'] )
|
||||||
|
* @property {String | Number} size 图标字体大小,单位px (默认 '16px' )
|
||||||
|
* @property {Boolean} bold 是否显示粗体 (默认 false )
|
||||||
|
* @property {String | Number} index 点击图标的时候传递事件出去的index(用于区分点击了哪一个)
|
||||||
|
* @property {String} hoverClass 图标按下去的样式类,用法同uni的view组件的hoverClass参数,详情见官网
|
||||||
|
* @property {String} customPrefix 自定义扩展前缀,方便用户扩展自己的图标库 (默认 'uicon' )
|
||||||
|
* @property {String | Number} label 图标右侧的label文字
|
||||||
|
* @property {String} labelPos label相对于图标的位置,只能right或bottom (默认 'right' )
|
||||||
|
* @property {String | Number} labelSize label字体大小,单位px (默认 '15px' )
|
||||||
|
* @property {String} labelColor 图标右侧的label文字颜色 ( 默认 color['uv-content-color'] )
|
||||||
|
* @property {String | Number} space label与图标的距离,单位px (默认 '3px' )
|
||||||
|
* @property {String} imgMode 图片的mode
|
||||||
|
* @property {String | Number} width 显示图片小图标时的宽度
|
||||||
|
* @property {String | Number} height 显示图片小图标时的高度
|
||||||
|
* @property {String | Number} top 图标在垂直方向上的定位 用于解决某些情况下,让图标垂直居中的用途 (默认 0 )
|
||||||
|
* @property {Boolean} stop 是否阻止事件传播 (默认 false )
|
||||||
|
* @property {Object} customStyle icon的样式,对象形式
|
||||||
|
* @event {Function} click 点击图标时触发
|
||||||
|
* @event {Function} touchstart 事件触摸时触发
|
||||||
|
* @example <uv-icon name="photo" color="#2979ff" size="28"></uv-icon>
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: 'uv-icon',
|
||||||
|
emits: ['click'],
|
||||||
|
mixins: [mpMixin, mixin, props],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
colorType: [
|
||||||
|
'primary',
|
||||||
|
'success',
|
||||||
|
'info',
|
||||||
|
'error',
|
||||||
|
'warning'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
uClasses() {
|
||||||
|
let classes = []
|
||||||
|
classes.push(this.customPrefix)
|
||||||
|
classes.push(this.customPrefix + '-' + this.name)
|
||||||
|
// 主题色,通过类配置
|
||||||
|
if (this.color && this.colorType.includes(this.color)) classes.push('uv-icon__icon--' + this.color)
|
||||||
|
// 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别
|
||||||
|
// 故需将其拆成一个字符串的形式,通过空格隔开各个类名
|
||||||
|
//#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
|
||||||
|
classes = classes.join(' ')
|
||||||
|
//#endif
|
||||||
|
return classes
|
||||||
|
},
|
||||||
|
iconStyle() {
|
||||||
|
let style = {}
|
||||||
|
style = {
|
||||||
|
fontSize: this.$uv.addUnit(this.size),
|
||||||
|
lineHeight: this.$uv.addUnit(this.size),
|
||||||
|
fontWeight: this.bold ? 'bold' : 'normal',
|
||||||
|
// 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中
|
||||||
|
top: this.$uv.addUnit(this.top)
|
||||||
|
}
|
||||||
|
// 非主题色值时,才当作颜色值
|
||||||
|
if (this.color && !this.colorType.includes(this.color)) style.color = this.color
|
||||||
|
return style
|
||||||
|
},
|
||||||
|
// 判断传入的name属性,是否图片路径,只要带有"/"均认为是图片形式
|
||||||
|
isImg() {
|
||||||
|
const isBase64 = this.name.indexOf('data:') > -1 && this.name.indexOf('base64') > -1;
|
||||||
|
return this.name.indexOf('/') !== -1 || isBase64;
|
||||||
|
},
|
||||||
|
imgStyle() {
|
||||||
|
let style = {}
|
||||||
|
// 如果设置width和height属性,则优先使用,否则使用size属性
|
||||||
|
style.width = this.width ? this.$uv.addUnit(this.width) : this.$uv.addUnit(this.size)
|
||||||
|
style.height = this.height ? this.$uv.addUnit(this.height) : this.$uv.addUnit(this.size)
|
||||||
|
return style
|
||||||
|
},
|
||||||
|
// 通过图标名,查找对应的图标
|
||||||
|
icon() {
|
||||||
|
// 如果内置的图标中找不到对应的图标,就直接返回name值,因为用户可能传入的是unicode代码
|
||||||
|
const code = icons['uvicon-' + this.name];
|
||||||
|
// #ifdef APP-NVUE
|
||||||
|
if(!code) {
|
||||||
|
return code ? unescape(`%u${code}`) : ['uvicon'].indexOf(this.customPrefix) > -1 ? unescape(`%u${this.name}`) : '';
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
return code ? unescape(`%u${code}`) : ['uvicon'].indexOf(this.customPrefix) > -1 ? this.name : '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickHandler(e) {
|
||||||
|
this.$emit('click', this.index)
|
||||||
|
// 是否阻止事件冒泡
|
||||||
|
this.stop && this.preventEvent(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
|
||||||
|
@import '@/uni_modules/uv-ui-tools/libs/css/color.scss';
|
||||||
|
// 变量定义
|
||||||
|
$uv-icon-primary: $uv-primary !default;
|
||||||
|
$uv-icon-success: $uv-success !default;
|
||||||
|
$uv-icon-info: $uv-info !default;
|
||||||
|
$uv-icon-warning: $uv-warning !default;
|
||||||
|
$uv-icon-error: $uv-error !default;
|
||||||
|
$uv-icon-label-line-height: 1 !default;
|
||||||
|
/* #ifndef APP-NVUE */
|
||||||
|
// 非nvue下加载字体
|
||||||
|
@font-face {
|
||||||
|
font-family: 'uvicon-iconfont';
|
||||||
|
src: url('./uvicons.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
/* #endif */
|
||||||
|
.uv-icon {
|
||||||
|
/* #ifndef APP-NVUE */
|
||||||
|
display: flex;
|
||||||
|
/* #endif */
|
||||||
|
align-items: center;
|
||||||
|
&--left {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
&--right {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
&--top {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
&--bottom {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
&__icon {
|
||||||
|
font-family: uvicon-iconfont;
|
||||||
|
position: relative;
|
||||||
|
@include flex;
|
||||||
|
align-items: center;
|
||||||
|
&--primary {
|
||||||
|
color: $uv-icon-primary;
|
||||||
|
}
|
||||||
|
&--success {
|
||||||
|
color: $uv-icon-success;
|
||||||
|
}
|
||||||
|
&--error {
|
||||||
|
color: $uv-icon-error;
|
||||||
|
}
|
||||||
|
&--warning {
|
||||||
|
color: $uv-icon-warning;
|
||||||
|
}
|
||||||
|
&--info {
|
||||||
|
color: $uv-icon-info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__img {
|
||||||
|
/* #ifndef APP-NVUE */
|
||||||
|
height: auto;
|
||||||
|
will-change: transform;
|
||||||
|
/* #endif */
|
||||||
|
}
|
||||||
|
&__label {
|
||||||
|
/* #ifndef APP-NVUE */
|
||||||
|
line-height: $uv-icon-label-line-height;
|
||||||
|
/* #endif */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
BIN
uni_modules/uv-icon/components/uv-icon/uvicons.ttf
Normal file
83
uni_modules/uv-icon/package.json
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"id": "uv-icon",
|
||||||
|
"displayName": "uv-icon 图标 全面兼容vue3+2、app、h5、小程序等多端",
|
||||||
|
"version": "1.0.13",
|
||||||
|
"description": "基于字体的图标集,包含了大多数常见场景的图标,支持自定义,支持自定义图片图标等。可自定义颜色、大小。",
|
||||||
|
"keywords": [
|
||||||
|
"uv-ui,uvui,uv-icon,icon,图标,字体图标"
|
||||||
|
],
|
||||||
|
"repository": "",
|
||||||
|
"engines": {
|
||||||
|
"HBuilderX": "^3.1.0"
|
||||||
|
},
|
||||||
|
"dcloudext": {
|
||||||
|
"type": "component-vue",
|
||||||
|
"sale": {
|
||||||
|
"regular": {
|
||||||
|
"price": "0.00"
|
||||||
|
},
|
||||||
|
"sourcecode": {
|
||||||
|
"price": "0.00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"qq": ""
|
||||||
|
},
|
||||||
|
"declaration": {
|
||||||
|
"ads": "无",
|
||||||
|
"data": "插件不采集任何数据",
|
||||||
|
"permissions": "无"
|
||||||
|
},
|
||||||
|
"npmurl": ""
|
||||||
|
},
|
||||||
|
"uni_modules": {
|
||||||
|
"dependencies": [
|
||||||
|
"uv-ui-tools"
|
||||||
|
],
|
||||||
|
"encrypt": [],
|
||||||
|
"platforms": {
|
||||||
|
"cloud": {
|
||||||
|
"tcb": "y",
|
||||||
|
"aliyun": "y"
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"Vue": {
|
||||||
|
"vue2": "y",
|
||||||
|
"vue3": "y"
|
||||||
|
},
|
||||||
|
"App": {
|
||||||
|
"app-vue": "y",
|
||||||
|
"app-nvue": "y"
|
||||||
|
},
|
||||||
|
"H5-mobile": {
|
||||||
|
"Safari": "y",
|
||||||
|
"Android Browser": "y",
|
||||||
|
"微信浏览器(Android)": "y",
|
||||||
|
"QQ浏览器(Android)": "y"
|
||||||
|
},
|
||||||
|
"H5-pc": {
|
||||||
|
"Chrome": "y",
|
||||||
|
"IE": "y",
|
||||||
|
"Edge": "y",
|
||||||
|
"Firefox": "y",
|
||||||
|
"Safari": "y"
|
||||||
|
},
|
||||||
|
"小程序": {
|
||||||
|
"微信": "y",
|
||||||
|
"阿里": "y",
|
||||||
|
"百度": "y",
|
||||||
|
"字节跳动": "y",
|
||||||
|
"QQ": "y",
|
||||||
|
"钉钉": "u",
|
||||||
|
"快手": "u",
|
||||||
|
"飞书": "u",
|
||||||
|
"京东": "u"
|
||||||
|
},
|
||||||
|
"快应用": {
|
||||||
|
"华为": "u",
|
||||||
|
"联盟": "u"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
uni_modules/uv-icon/readme.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
## uv-icon 图标库
|
||||||
|
|
||||||
|
> **组件名:uv-icon**
|
||||||
|
|
||||||
|
基于字体的图标集,包含了大多数常见场景的图标,支持自定义,支持自定义图片图标等。
|
||||||
|
|
||||||
|
# <a href="https://www.uvui.cn/components/icon.html" target="_blank">查看文档</a>
|
||||||
|
|
||||||
|
## [下载完整示例项目](https://ext.dcloud.net.cn/plugin?name=uv-ui)
|
||||||
|
|
||||||
|
### [更多插件,请关注uv-ui组件库](https://ext.dcloud.net.cn/plugin?name=uv-ui)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 如使用过程中有任何问题反馈,或者您对uv-ui有一些好的建议,欢迎加入uv-ui官方交流群:<a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a>
|
||||||
29
uni_modules/uv-input/changelog.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
## 1.0.13(2023-12-06)
|
||||||
|
1. 优化
|
||||||
|
## 1.0.12(2023-12-06)
|
||||||
|
1. 阻止事件冒泡问题
|
||||||
|
## 1.0.11(2023-11-10)
|
||||||
|
1. 调整清除按钮样式的marginLeft,避免微信上多数情况触发不了的BUG
|
||||||
|
## 1.0.10(2023-10-07)
|
||||||
|
1. 修复搜狗输入法下存在不可清空的情况
|
||||||
|
## 1.0.9(2023-09-14)
|
||||||
|
1. 修复H5等情况设置禁用或可读情况下,点击事件无效的问题
|
||||||
|
## 1.0.8(2023-08-22)
|
||||||
|
1. 修复无法@keyboardheightchange无法获取键盘高度的BUG
|
||||||
|
## 1.0.7(2023-08-18)
|
||||||
|
1. 修复ios端不能输入的BUG
|
||||||
|
## 1.0.6(2023-08-05)
|
||||||
|
1. 修复在vue2模式下,v-model设置为0时不生效的BUG
|
||||||
|
## 1.0.5(2023-07-18)
|
||||||
|
1. 修复在微信小程序端清除内容存在不能清除的BUG
|
||||||
|
## 1.0.4(2023-07-13)
|
||||||
|
1. 修复value/v-model更改不生效的BUG
|
||||||
|
## 1.0.3(2023-07-03)
|
||||||
|
去除插槽判断,避免某些平台不显示的BUG
|
||||||
|
## 1.0.2(2023-05-16)
|
||||||
|
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
|
||||||
|
2. 优化部分功能
|
||||||
|
## 1.0.1(2023-05-12)
|
||||||
|
1. 修复vue3双向绑定的BUG
|
||||||
|
## 1.0.0(2023-05-10)
|
||||||
|
uv-input 输入框
|
||||||
175
uni_modules/uv-input/components/uv-input/props.js
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 输入框类型
|
||||||
|
// number-数字输入键盘,app-vue下可以输入浮点数,app-nvue和小程序平台下只能输入整数
|
||||||
|
// idcard-身份证输入键盘,微信、支付宝、百度、QQ小程序
|
||||||
|
// digit-带小数点的数字键盘,App的nvue页面、微信、支付宝、百度、头条、QQ小程序
|
||||||
|
// text-文本输入键盘
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'text'
|
||||||
|
},
|
||||||
|
// 是否禁用输入框
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 禁用状态时的背景色
|
||||||
|
disabledColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#f5f7fa'
|
||||||
|
},
|
||||||
|
// 是否显示清除控件
|
||||||
|
clearable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 是否密码类型
|
||||||
|
password: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 最大输入长度,设置为 -1 的时候不限制最大长度
|
||||||
|
maxlength: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: -1
|
||||||
|
},
|
||||||
|
// 输入框为空时的占位符
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
// 指定placeholder的样式类,注意页面或组件的style中写了scoped时,需要在类名前写/deep/
|
||||||
|
placeholderClass: {
|
||||||
|
type: String,
|
||||||
|
default: 'input-placeholder'
|
||||||
|
},
|
||||||
|
// 指定placeholder的样式
|
||||||
|
placeholderStyle: {
|
||||||
|
type: [String, Object],
|
||||||
|
default: 'color: #c0c4cc'
|
||||||
|
},
|
||||||
|
// 设置右下角按钮的文字,有效值:send|search|next|go|done,兼容性详见uni-app文档
|
||||||
|
// https://uniapp.dcloud.io/component/input
|
||||||
|
// https://uniapp.dcloud.io/component/textarea
|
||||||
|
confirmType: {
|
||||||
|
type: String,
|
||||||
|
default: 'done'
|
||||||
|
},
|
||||||
|
// 点击键盘右下角按钮时是否保持键盘不收起,H5无效
|
||||||
|
confirmHold: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// focus时,点击页面的时候不收起键盘,微信小程序有效
|
||||||
|
holdKeyboard: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 自动获取焦点
|
||||||
|
// 在 H5 平台能否聚焦以及软键盘是否跟随弹出,取决于当前浏览器本身的实现。nvue 页面不支持,需使用组件的 focus()、blur() 方法控制焦点
|
||||||
|
focus: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 键盘收起时,是否自动失去焦点,目前仅App3.0.0+有效
|
||||||
|
autoBlur: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 指定focus时光标的位置
|
||||||
|
cursor: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: -1
|
||||||
|
},
|
||||||
|
// 输入框聚焦时底部与键盘的距离
|
||||||
|
cursorSpacing: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 30
|
||||||
|
},
|
||||||
|
// 光标起始位置,自动聚集时有效,需与selection-end搭配使用
|
||||||
|
selectionStart: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: -1
|
||||||
|
},
|
||||||
|
// 光标结束位置,自动聚集时有效,需与selection-start搭配使用
|
||||||
|
selectionEnd: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: -1
|
||||||
|
},
|
||||||
|
// 键盘弹起时,是否自动上推页面
|
||||||
|
adjustPosition: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 输入框内容对齐方式,可选值为:left|center|right
|
||||||
|
inputAlign: {
|
||||||
|
type: String,
|
||||||
|
default: 'left'
|
||||||
|
},
|
||||||
|
// 输入框字体的大小
|
||||||
|
fontSize: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: '14px'
|
||||||
|
},
|
||||||
|
// 输入框字体颜色
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: '#303133'
|
||||||
|
},
|
||||||
|
// 输入框前置图标
|
||||||
|
prefixIcon: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 前置图标样式,对象或字符串
|
||||||
|
prefixIconStyle: {
|
||||||
|
type: [String, Object],
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 输入框后置图标
|
||||||
|
suffixIcon: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 后置图标样式,对象或字符串
|
||||||
|
suffixIconStyle: {
|
||||||
|
type: [String, Object],
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 边框类型,surround-四周边框,bottom-底部边框,none-无边框
|
||||||
|
border: {
|
||||||
|
type: String,
|
||||||
|
default: 'surround'
|
||||||
|
},
|
||||||
|
// 是否只读,与disabled不同之处在于disabled会置灰组件,而readonly则不会
|
||||||
|
readonly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 输入框形状,circle-圆形,square-方形
|
||||||
|
shape: {
|
||||||
|
type: String,
|
||||||
|
default: 'square'
|
||||||
|
},
|
||||||
|
// 用于处理或者过滤输入框内容的方法
|
||||||
|
formatter: {
|
||||||
|
type: [Function, null],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
// 是否忽略组件内对文本合成系统事件的处理
|
||||||
|
ignoreCompositionEvent: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
...uni.$uv?.props?.input
|
||||||
|
}
|
||||||
|
}
|
||||||
348
uni_modules/uv-input/components/uv-input/uv-input.vue
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
<template>
|
||||||
|
<view class="uv-input" :class="inputClass" :style="[wrapperStyle]">
|
||||||
|
<view class="uv-input__content">
|
||||||
|
<view class="uv-input__content__prefix-icon">
|
||||||
|
<slot name="prefix">
|
||||||
|
<uv-icon
|
||||||
|
v-if="prefixIcon"
|
||||||
|
:name="prefixIcon"
|
||||||
|
size="18"
|
||||||
|
:customStyle="prefixIconStyle"
|
||||||
|
></uv-icon>
|
||||||
|
</slot>
|
||||||
|
</view>
|
||||||
|
<view class="uv-input__content__field-wrapper" @click="clickHandler">
|
||||||
|
<!-- 根据uni-app的input组件文档,H5和APP中只要声明了password参数(无论true还是false),type均失效,此时
|
||||||
|
为了防止type=number时,又存在password属性,type无效,此时需要设置password为undefined
|
||||||
|
-->
|
||||||
|
<input
|
||||||
|
class="uv-input__content__field-wrapper__field"
|
||||||
|
:style="[inputStyle]"
|
||||||
|
:type="type"
|
||||||
|
:focus="focus"
|
||||||
|
:cursor="cursor"
|
||||||
|
:value="innerValue"
|
||||||
|
:auto-blur="autoBlur"
|
||||||
|
:disabled="disabled || readonly"
|
||||||
|
:maxlength="maxlength"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:placeholder-style="placeholderStyle"
|
||||||
|
:placeholder-class="placeholderClass"
|
||||||
|
:confirm-type="confirmType"
|
||||||
|
:confirm-hold="confirmHold"
|
||||||
|
:hold-keyboard="holdKeyboard"
|
||||||
|
:cursor-spacing="cursorSpacing"
|
||||||
|
:adjust-position="adjustPosition"
|
||||||
|
:selection-end="selectionEnd"
|
||||||
|
:selection-start="selectionStart"
|
||||||
|
:password="password || type === 'password' || undefined"
|
||||||
|
:ignoreCompositionEvent="ignoreCompositionEvent"
|
||||||
|
@input="onInput"
|
||||||
|
@blur="onBlur"
|
||||||
|
@focus="onFocus"
|
||||||
|
@confirm="onConfirm"
|
||||||
|
@keyboardheightchange="onkeyboardheightchange"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="uv-input__content__clear"
|
||||||
|
v-if="isShowClear"
|
||||||
|
@tap="onClear"
|
||||||
|
>
|
||||||
|
<uv-icon
|
||||||
|
name="close"
|
||||||
|
size="11"
|
||||||
|
color="#ffffff"
|
||||||
|
customStyle="line-height: 12px"
|
||||||
|
></uv-icon>
|
||||||
|
</view>
|
||||||
|
<view class="uv-input__content__subfix-icon">
|
||||||
|
<slot name="suffix">
|
||||||
|
<uv-icon
|
||||||
|
v-if="suffixIcon"
|
||||||
|
:name="suffixIcon"
|
||||||
|
size="18"
|
||||||
|
:customStyle="suffixIconStyle"
|
||||||
|
></uv-icon>
|
||||||
|
</slot>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
|
||||||
|
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
|
||||||
|
import props from "./props.js";
|
||||||
|
/**
|
||||||
|
* Input 输入框
|
||||||
|
* @description 此组件为一个输入框,默认没有边框和样式,是专门为配合表单组件uv-form而设计的,利用它可以快速实现表单验证,输入内容,下拉选择等功能。
|
||||||
|
* @tutorial https://www.uvui.cn/components/input.html
|
||||||
|
* @property {String | Number} value 输入的值
|
||||||
|
* @property {String} type 输入框类型,见上方说明 ( 默认 'text' )
|
||||||
|
* @property {Boolean} fixed 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true,兼容性:微信小程序、百度小程序、字节跳动小程序、QQ小程序 ( 默认 false )
|
||||||
|
* @property {Boolean} disabled 是否禁用输入框 ( 默认 false )
|
||||||
|
* @property {String} disabledColor 禁用状态时的背景色( 默认 '#f5f7fa' )
|
||||||
|
* @property {Boolean} clearable 是否显示清除控件 ( 默认 false )
|
||||||
|
* @property {Boolean} password 是否密码类型 ( 默认 false )
|
||||||
|
* @property {String | Number} maxlength 最大输入长度,设置为 -1 的时候不限制最大长度 ( 默认 -1 )
|
||||||
|
* @property {String} placeholder 输入框为空时的占位符
|
||||||
|
* @property {String} placeholderClass 指定placeholder的样式类,注意页面或组件的style中写了scoped时,需要在类名前写/deep/ ( 默认 'input-placeholder' )
|
||||||
|
* @property {String | Object} placeholderStyle 指定placeholder的样式,字符串/对象形式,如"color: red;"
|
||||||
|
* @property {Boolean} showWordLimit 是否显示输入字数统计,只在 type ="text"或type ="textarea"时有效 ( 默认 false )
|
||||||
|
* @property {String} confirmType 设置右下角按钮的文字,兼容性详见uni-app文档 ( 默认 'done' )
|
||||||
|
* @property {Boolean} confirmHold 点击键盘右下角按钮时是否保持键盘不收起,H5无效 ( 默认 false )
|
||||||
|
* @property {Boolean} holdKeyboard focus时,点击页面的时候不收起键盘,微信小程序有效 ( 默认 false )
|
||||||
|
* @property {Boolean} focus 自动获取焦点,在 H5 平台能否聚焦以及软键盘是否跟随弹出,取决于当前浏览器本身的实现。nvue 页面不支持,需使用组件的 focus()、blur() 方法控制焦点 ( 默认 false )
|
||||||
|
* @property {Boolean} autoBlur 键盘收起时,是否自动失去焦点,目前仅App3.0.0+有效 ( 默认 false )
|
||||||
|
* @property {Boolean} disableDefaultPadding 是否去掉 iOS 下的默认内边距,仅微信小程序,且type=textarea时有效 ( 默认 false )
|
||||||
|
* @property {String | Number} cursor 指定focus时光标的位置( 默认 -1 )
|
||||||
|
* @property {String | Number} cursorSpacing 输入框聚焦时底部与键盘的距离 ( 默认 30 )
|
||||||
|
* @property {String | Number} selectionStart 光标起始位置,自动聚集时有效,需与selection-end搭配使用 ( 默认 -1 )
|
||||||
|
* @property {String | Number} selectionEnd 光标结束位置,自动聚集时有效,需与selection-start搭配使用 ( 默认 -1 )
|
||||||
|
* @property {Boolean} adjustPosition 键盘弹起时,是否自动上推页面 ( 默认 true )
|
||||||
|
* @property {String} inputAlign 输入框内容对齐方式( 默认 'left' )
|
||||||
|
* @property {String | Number} fontSize 输入框字体的大小 ( 默认 '15px' )
|
||||||
|
* @property {String} color 输入框字体颜色 ( 默认 '#303133' )
|
||||||
|
* @property {Function} formatter 内容式化函数
|
||||||
|
* @property {String} prefixIcon 输入框前置图标
|
||||||
|
* @property {String | Object} prefixIconStyle 前置图标样式,对象或字符串
|
||||||
|
* @property {String} suffixIcon 输入框后置图标
|
||||||
|
* @property {String | Object} suffixIconStyle 后置图标样式,对象或字符串
|
||||||
|
* @property {String} border 边框类型,surround-四周边框,bottom-底部边框,none-无边框 ( 默认 'surround' )
|
||||||
|
* @property {Boolean} readonly 是否只读,与disabled不同之处在于disabled会置灰组件,而readonly则不会 ( 默认 false )
|
||||||
|
* @property {String} shape 输入框形状,circle-圆形,square-方形 ( 默认 'square' )
|
||||||
|
* @property {Object} customStyle 定义需要用到的外部样式
|
||||||
|
* @property {Boolean} ignoreCompositionEvent 是否忽略组件内对文本合成系统事件的处理。
|
||||||
|
* @example <uv-input v-model="value" :password="true" suffix-icon="lock-fill" />
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: "uv-input",
|
||||||
|
mixins: [mpMixin, mixin, props],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// 输入框的值
|
||||||
|
innerValue: "",
|
||||||
|
// 是否处于获得焦点状态
|
||||||
|
focused: false,
|
||||||
|
// 过滤处理方法
|
||||||
|
innerFormatter: value => value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// #ifdef VUE2
|
||||||
|
this.innerValue = this.value;
|
||||||
|
// #endif
|
||||||
|
// #ifdef VUE3
|
||||||
|
this.innerValue = this.modelValue;
|
||||||
|
// #endif
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value(newVal){
|
||||||
|
this.innerValue = newVal;
|
||||||
|
},
|
||||||
|
modelValue(newVal){
|
||||||
|
this.innerValue = newVal;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
// 是否显示清除控件
|
||||||
|
isShowClear() {
|
||||||
|
const { clearable, readonly, focused, innerValue } = this;
|
||||||
|
return !!clearable && !readonly && !!focused && innerValue !== "";
|
||||||
|
},
|
||||||
|
// 组件的类名
|
||||||
|
inputClass() {
|
||||||
|
let classes = [],
|
||||||
|
{ border, disabled, shape } = this;
|
||||||
|
border === "surround" &&
|
||||||
|
(classes = classes.concat(["uv-border", "uv-input--radius"]));
|
||||||
|
classes.push(`uv-input--${shape}`);
|
||||||
|
border === "bottom" &&
|
||||||
|
(classes = classes.concat([
|
||||||
|
"uv-border-bottom",
|
||||||
|
"uv-input--no-radius",
|
||||||
|
]));
|
||||||
|
return classes.join(" ");
|
||||||
|
},
|
||||||
|
// 组件的样式
|
||||||
|
wrapperStyle() {
|
||||||
|
const style = {};
|
||||||
|
// 禁用状态下,被背景色加上对应的样式
|
||||||
|
if (this.disabled) {
|
||||||
|
style.backgroundColor = this.disabledColor;
|
||||||
|
}
|
||||||
|
// 无边框时,去除内边距
|
||||||
|
if (this.border === "none") {
|
||||||
|
style.padding = "0";
|
||||||
|
} else {
|
||||||
|
// 由于uni-app的iOS开发者能力有限,导致需要分开写才有效
|
||||||
|
style.paddingTop = "6px";
|
||||||
|
style.paddingBottom = "6px";
|
||||||
|
style.paddingLeft = "9px";
|
||||||
|
style.paddingRight = "9px";
|
||||||
|
}
|
||||||
|
return this.$uv.deepMerge(style, this.$uv.addStyle(this.customStyle));
|
||||||
|
},
|
||||||
|
// 输入框的样式
|
||||||
|
inputStyle() {
|
||||||
|
const style = {
|
||||||
|
color: this.color,
|
||||||
|
fontSize: this.$uv.addUnit(this.fontSize),
|
||||||
|
textAlign: this.inputAlign
|
||||||
|
};
|
||||||
|
// #ifndef APP-NVUE
|
||||||
|
if(this.disabled || this.readonly) {
|
||||||
|
style['pointer-events'] = 'none';
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 在微信小程序中,不支持将函数当做props参数,故只能通过ref形式调用
|
||||||
|
setFormatter(e) {
|
||||||
|
this.innerFormatter = e
|
||||||
|
},
|
||||||
|
// 当键盘输入时,触发input事件
|
||||||
|
onInput(e) {
|
||||||
|
let { value = "" } = e.detail || {};
|
||||||
|
// 格式化过滤方法
|
||||||
|
const formatter = this.formatter || this.innerFormatter
|
||||||
|
const formatValue = formatter(value)
|
||||||
|
// 为了避免props的单向数据流特性,需要先将innerValue值设置为当前值,再在$nextTick中重新赋予设置后的值才有效
|
||||||
|
this.innerValue = value
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.innerValue = formatValue;
|
||||||
|
this.valueChange();
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 输入框失去焦点时触发
|
||||||
|
onBlur(event) {
|
||||||
|
this.$emit("blur", event.detail.value);
|
||||||
|
// H5端的blur会先于点击清除控件的点击click事件触发,导致focused
|
||||||
|
// 瞬间为false,从而隐藏了清除控件而无法被点击到
|
||||||
|
this.$uv.sleep(100).then(() => {
|
||||||
|
this.focused = false;
|
||||||
|
});
|
||||||
|
// 尝试调用uv-form的验证方法
|
||||||
|
this.$uv.formValidate(this, "blur");
|
||||||
|
},
|
||||||
|
// 输入框聚焦时触发
|
||||||
|
onFocus(event) {
|
||||||
|
this.focused = true;
|
||||||
|
this.$emit("focus");
|
||||||
|
},
|
||||||
|
// 点击完成按钮时触发
|
||||||
|
onConfirm(event) {
|
||||||
|
this.$emit("confirm", this.innerValue);
|
||||||
|
},
|
||||||
|
// 键盘高度发生变化的时候触发此事件
|
||||||
|
// 兼容性:微信小程序2.7.0+、App 3.1.0+
|
||||||
|
onkeyboardheightchange(e) {
|
||||||
|
this.$emit("keyboardheightchange",e);
|
||||||
|
},
|
||||||
|
// 内容发生变化,进行处理
|
||||||
|
valueChange() {
|
||||||
|
if(this.isClear) this.innerValue = '';
|
||||||
|
const value = this.innerValue;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$emit("input", value);
|
||||||
|
this.$emit("update:modelValue", value);
|
||||||
|
this.$emit("change", value);
|
||||||
|
// 尝试调用uv-form的验证方法
|
||||||
|
this.$uv.formValidate(this, "change");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// 点击清除控件
|
||||||
|
onClear() {
|
||||||
|
this.innerValue = "";
|
||||||
|
this.isClear = true;
|
||||||
|
this.$uv.sleep(200).then(res=>{
|
||||||
|
this.isClear = false;
|
||||||
|
})
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$emit("clear");
|
||||||
|
this.valueChange();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 在安卓nvue上,事件无法冒泡
|
||||||
|
* 在某些时间,我们希望监听uv-from-item的点击事件,此时会导致点击uv-form-item内的uv-input后
|
||||||
|
* 无法触发uv-form-item的点击事件,这里通过手动调用uv-form-item的方法进行触发
|
||||||
|
*/
|
||||||
|
clickHandler() {
|
||||||
|
// #ifdef APP-NVUE
|
||||||
|
if (this.$uv.os() === "android") {
|
||||||
|
const formItem = this.$uv.$parent.call(this, "uv-form-item");
|
||||||
|
if (formItem) {
|
||||||
|
formItem.clickHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$show-border: 1;
|
||||||
|
$show-border-surround: 1;
|
||||||
|
$show-border-bottom: 1;
|
||||||
|
@import '@/uni_modules/uv-ui-tools/libs/css/variable.scss';
|
||||||
|
@import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
|
||||||
|
@import '@/uni_modules/uv-ui-tools/libs/css/color.scss';
|
||||||
|
.uv-input {
|
||||||
|
@include flex(row);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex: 1;
|
||||||
|
&--radius,
|
||||||
|
&--square {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
&--no-radius {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
&--circle {
|
||||||
|
border-radius: 100px;
|
||||||
|
}
|
||||||
|
&__content {
|
||||||
|
flex: 1;
|
||||||
|
@include flex(row);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
&__field-wrapper {
|
||||||
|
position: relative;
|
||||||
|
@include flex(row);
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
&__field {
|
||||||
|
line-height: 26px;
|
||||||
|
text-align: left;
|
||||||
|
color: $uv-main-color;
|
||||||
|
height: 24px;
|
||||||
|
font-size: 15px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__clear {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 100px;
|
||||||
|
background-color: #c6c7cb;
|
||||||
|
@include flex(row);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transform: scale(0.82);
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
&__subfix-icon {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
&__prefix-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
88
uni_modules/uv-input/package.json
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
{
|
||||||
|
"id": "uv-input",
|
||||||
|
"displayName": "uv-input 输入框 全面兼容vue3+2、app、h5、小程序等多端",
|
||||||
|
"version": "1.0.13",
|
||||||
|
"description": "uv-input 该组件为一个输入框,默认没有边框和样式,是专门为配合表单组件uv-form而设计的,利用它可以快速实现表单验证,输入内容,下拉选择等功能。",
|
||||||
|
"keywords": [
|
||||||
|
"uv-input",
|
||||||
|
"uvui",
|
||||||
|
"uv-ui",
|
||||||
|
"input",
|
||||||
|
"输入框"
|
||||||
|
],
|
||||||
|
"repository": "",
|
||||||
|
"engines": {
|
||||||
|
"HBuilderX": "^3.1.0"
|
||||||
|
},
|
||||||
|
"dcloudext": {
|
||||||
|
"type": "component-vue",
|
||||||
|
"sale": {
|
||||||
|
"regular": {
|
||||||
|
"price": "0.00"
|
||||||
|
},
|
||||||
|
"sourcecode": {
|
||||||
|
"price": "0.00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"qq": ""
|
||||||
|
},
|
||||||
|
"declaration": {
|
||||||
|
"ads": "无",
|
||||||
|
"data": "插件不采集任何数据",
|
||||||
|
"permissions": "无"
|
||||||
|
},
|
||||||
|
"npmurl": ""
|
||||||
|
},
|
||||||
|
"uni_modules": {
|
||||||
|
"dependencies": [
|
||||||
|
"uv-ui-tools",
|
||||||
|
"uv-icon"
|
||||||
|
],
|
||||||
|
"encrypt": [],
|
||||||
|
"platforms": {
|
||||||
|
"cloud": {
|
||||||
|
"tcb": "y",
|
||||||
|
"aliyun": "y"
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"Vue": {
|
||||||
|
"vue2": "y",
|
||||||
|
"vue3": "y"
|
||||||
|
},
|
||||||
|
"App": {
|
||||||
|
"app-vue": "y",
|
||||||
|
"app-nvue": "y"
|
||||||
|
},
|
||||||
|
"H5-mobile": {
|
||||||
|
"Safari": "y",
|
||||||
|
"Android Browser": "y",
|
||||||
|
"微信浏览器(Android)": "y",
|
||||||
|
"QQ浏览器(Android)": "y"
|
||||||
|
},
|
||||||
|
"H5-pc": {
|
||||||
|
"Chrome": "y",
|
||||||
|
"IE": "y",
|
||||||
|
"Edge": "y",
|
||||||
|
"Firefox": "y",
|
||||||
|
"Safari": "y"
|
||||||
|
},
|
||||||
|
"小程序": {
|
||||||
|
"微信": "y",
|
||||||
|
"阿里": "y",
|
||||||
|
"百度": "y",
|
||||||
|
"字节跳动": "y",
|
||||||
|
"QQ": "y",
|
||||||
|
"钉钉": "u",
|
||||||
|
"快手": "u",
|
||||||
|
"飞书": "u",
|
||||||
|
"京东": "u"
|
||||||
|
},
|
||||||
|
"快应用": {
|
||||||
|
"华为": "u",
|
||||||
|
"联盟": "u"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
uni_modules/uv-input/readme.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
## Input 输入框
|
||||||
|
|
||||||
|
> **组件名:uv-input**
|
||||||
|
|
||||||
|
此组件为一个输入框,默认没有边框和样式,是专门为配合表单组件uv-form而设计的,利用它可以快速实现表单验证,输入内容,下拉选择等功能。
|
||||||
|
|
||||||
|
# <a href="https://www.uvui.cn/components/input.html" target="_blank">查看文档</a>
|
||||||
|
|
||||||
|
## [下载完整示例项目](https://ext.dcloud.net.cn/plugin?name=uv-ui) <small>(请不要 下载插件ZIP)</small>
|
||||||
|
|
||||||
|
### [更多插件,请关注uv-ui组件库](https://ext.dcloud.net.cn/plugin?name=uv-ui)
|
||||||
|
|
||||||
|
<a href="https://ext.dcloud.net.cn/plugin?name=uv-ui" target="_blank">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
#### 如使用过程中有任何问题反馈,或者您对uv-ui有一些好的建议,欢迎加入uv-ui官方交流群:<a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a>
|
||||||
9
uni_modules/uv-loading-icon/changelog.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
## 1.0.3(2023-08-14)
|
||||||
|
1. 新增参数textStyle,自定义文本样式
|
||||||
|
## 1.0.2(2023-06-27)
|
||||||
|
优化
|
||||||
|
## 1.0.1(2023-05-16)
|
||||||
|
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
|
||||||
|
2. 优化部分功能
|
||||||
|
## 1.0.0(2023-05-10)
|
||||||
|
1. 新增uv-loading-icon组件
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
// 是否显示组件
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 颜色
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: '#909193'
|
||||||
|
},
|
||||||
|
// 提示文字颜色
|
||||||
|
textColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#909193'
|
||||||
|
},
|
||||||
|
// 文字和图标是否垂直排列
|
||||||
|
vertical: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 模式选择,circle-圆形,spinner-花朵形,semicircle-半圆形
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: 'spinner'
|
||||||
|
},
|
||||||
|
// 图标大小,单位默认px
|
||||||
|
size: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 24
|
||||||
|
},
|
||||||
|
// 文字大小
|
||||||
|
textSize: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 15
|
||||||
|
},
|
||||||
|
// 文字样式
|
||||||
|
textStyle: {
|
||||||
|
type: Object,
|
||||||
|
default () {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 文字内容
|
||||||
|
text: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 动画模式 https://www.runoob.com/cssref/css3-pr-animation-timing-function.html
|
||||||
|
timingFunction: {
|
||||||
|
type: String,
|
||||||
|
default: 'linear'
|
||||||
|
},
|
||||||
|
// 动画执行周期时间
|
||||||
|
duration: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 1200
|
||||||
|
},
|
||||||
|
// mode=circle时的暗边颜色
|
||||||
|
inactiveColor: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
...uni.$uv?.props?.loadingIcon
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
class="uv-loading-icon"
|
||||||
|
:style="[$uv.addStyle(customStyle)]"
|
||||||
|
:class="[vertical && 'uv-loading-icon--vertical']"
|
||||||
|
v-if="show"
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
v-if="!webviewHide"
|
||||||
|
class="uv-loading-icon__spinner"
|
||||||
|
:class="[`uv-loading-icon__spinner--${mode}`]"
|
||||||
|
ref="ani"
|
||||||
|
:style="{
|
||||||
|
color: color,
|
||||||
|
width: $uv.addUnit(size),
|
||||||
|
height: $uv.addUnit(size),
|
||||||
|
borderTopColor: color,
|
||||||
|
borderBottomColor: otherBorderColor,
|
||||||
|
borderLeftColor: otherBorderColor,
|
||||||
|
borderRightColor: otherBorderColor,
|
||||||
|
'animation-duration': `${duration}ms`,
|
||||||
|
'animation-timing-function': mode === 'semicircle' || mode === 'circle' ? timingFunction : ''
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<block v-if="mode === 'spinner'">
|
||||||
|
<!-- #ifndef APP-NVUE -->
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in array12"
|
||||||
|
:key="index"
|
||||||
|
class="uv-loading-icon__dot"
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
<!-- #endif -->
|
||||||
|
<!-- #ifdef APP-NVUE -->
|
||||||
|
<!-- 此组件内部图标部分无法设置宽高,即使通过width和height配置了也无效 -->
|
||||||
|
<loading-indicator
|
||||||
|
v-if="!webviewHide"
|
||||||
|
class="uv-loading-indicator"
|
||||||
|
:animating="true"
|
||||||
|
:style="{
|
||||||
|
color: color,
|
||||||
|
width: $uv.addUnit(size),
|
||||||
|
height: $uv.addUnit(size)
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<!-- #endif -->
|
||||||
|
</block>
|
||||||
|
</view>
|
||||||
|
<text
|
||||||
|
v-if="text"
|
||||||
|
class="uv-loading-icon__text"
|
||||||
|
:style="[{
|
||||||
|
fontSize: $uv.addUnit(textSize),
|
||||||
|
color: textColor,
|
||||||
|
},$uv.addStyle(textStyle)]"
|
||||||
|
>{{text}}</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { colorGradient } from '@/uni_modules/uv-ui-tools/libs/function/colorGradient.js'
|
||||||
|
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
|
||||||
|
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
|
||||||
|
import props from './props.js';
|
||||||
|
// #ifdef APP-NVUE
|
||||||
|
const animation = weex.requireModule('animation');
|
||||||
|
// #endif
|
||||||
|
/**
|
||||||
|
* loading 加载动画
|
||||||
|
* @description 警此组件为一个小动画,目前用在uvui的loadmore加载更多和switch开关等组件的正在加载状态场景。
|
||||||
|
* @tutorial https://www.uvui.cn/components/loading.html
|
||||||
|
* @property {Boolean} show 是否显示组件 (默认 true)
|
||||||
|
* @property {String} color 动画活动区域的颜色,只对 mode = flower 模式有效(默认#909193)
|
||||||
|
* @property {String} textColor 提示文本的颜色(默认#909193)
|
||||||
|
* @property {Boolean} vertical 文字和图标是否垂直排列 (默认 false )
|
||||||
|
* @property {String} mode 模式选择,见官网说明(默认 'circle' )
|
||||||
|
* @property {String | Number} size 加载图标的大小,单位px (默认 24 )
|
||||||
|
* @property {String | Number} textSize 文字大小(默认 15 )
|
||||||
|
* @property {String | Number} text 文字内容
|
||||||
|
* @property {Object} textStyle 文字样式
|
||||||
|
* @property {String} timingFunction 动画模式 (默认 'ease-in-out' )
|
||||||
|
* @property {String | Number} duration 动画执行周期时间(默认 1200)
|
||||||
|
* @property {String} inactiveColor mode=circle时的暗边颜色
|
||||||
|
* @property {Object} customStyle 定义需要用到的外部样式
|
||||||
|
* @example <uv-loading mode="circle"></uv-loading>
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: 'uv-loading-icon',
|
||||||
|
mixins: [mpMixin, mixin, props],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// Array.form可以通过一个伪数组对象创建指定长度的数组
|
||||||
|
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/from
|
||||||
|
array12: Array.from({
|
||||||
|
length: 12
|
||||||
|
}),
|
||||||
|
// 这里需要设置默认值为360,否则在安卓nvue上,会延迟一个duration周期后才执行
|
||||||
|
// 在iOS nvue上,则会一开始默认执行两个周期的动画
|
||||||
|
aniAngel: 360, // 动画旋转角度
|
||||||
|
webviewHide: false, // 监听webview的状态,如果隐藏了页面,则停止动画,以免性能消耗
|
||||||
|
loading: false, // 是否运行中,针对nvue使用
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
// 当为circle类型时,给其另外三边设置一个更轻一些的颜色
|
||||||
|
// 之所以需要这么做的原因是,比如父组件传了color为红色,那么需要另外的三个边为浅红色
|
||||||
|
// 而不能是固定的某一个其他颜色(因为这个固定的颜色可能浅蓝,导致效果没有那么细腻良好)
|
||||||
|
otherBorderColor() {
|
||||||
|
const lightColor = colorGradient(this.color, '#ffffff', 100)[80]
|
||||||
|
if (this.mode === 'circle') {
|
||||||
|
return this.inactiveColor ? this.inactiveColor : lightColor
|
||||||
|
} else {
|
||||||
|
return 'transparent'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show(n) {
|
||||||
|
// nvue中,show为true,且为非loading状态,就重新执行动画模块
|
||||||
|
// #ifdef APP-NVUE
|
||||||
|
if (n && !this.loading) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.startAnimate()
|
||||||
|
}, 30)
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
init() {
|
||||||
|
setTimeout(() => {
|
||||||
|
// #ifdef APP-NVUE
|
||||||
|
this.show && this.nvueAnimate()
|
||||||
|
// #endif
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
this.show && this.addEventListenerToWebview()
|
||||||
|
// #endif
|
||||||
|
}, 20)
|
||||||
|
},
|
||||||
|
// 监听webview的显示与隐藏
|
||||||
|
addEventListenerToWebview() {
|
||||||
|
// webview的堆栈
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
// 当前页面
|
||||||
|
const page = pages[pages.length - 1]
|
||||||
|
// 当前页面的webview实例
|
||||||
|
const currentWebview = page.$getAppWebview()
|
||||||
|
// 监听webview的显示与隐藏,从而停止或者开始动画(为了性能)
|
||||||
|
currentWebview.addEventListener('hide', () => {
|
||||||
|
this.webviewHide = true
|
||||||
|
})
|
||||||
|
currentWebview.addEventListener('show', () => {
|
||||||
|
this.webviewHide = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// #ifdef APP-NVUE
|
||||||
|
nvueAnimate() {
|
||||||
|
// nvue下,非spinner类型时才需要旋转,因为nvue的spinner类型,使用了weex的
|
||||||
|
// loading-indicator组件,自带旋转功能
|
||||||
|
this.mode !== 'spinner' && this.startAnimate()
|
||||||
|
},
|
||||||
|
// 执行nvue的animate模块动画
|
||||||
|
startAnimate() {
|
||||||
|
this.loading = true
|
||||||
|
const ani = this.$refs.ani
|
||||||
|
if (!ani) return
|
||||||
|
animation.transition(ani, {
|
||||||
|
// 进行角度旋转
|
||||||
|
styles: {
|
||||||
|
transform: `rotate(${this.aniAngel}deg)`,
|
||||||
|
transformOrigin: 'center center'
|
||||||
|
},
|
||||||
|
duration: this.duration,
|
||||||
|
timingFunction: this.timingFunction,
|
||||||
|
// delay: 10
|
||||||
|
}, () => {
|
||||||
|
// 每次增加360deg,为了让其重新旋转一周
|
||||||
|
this.aniAngel += 360
|
||||||
|
// 动画结束后,继续循环执行动画,需要同时判断webviewHide变量
|
||||||
|
// nvue安卓,页面隐藏后依然会继续执行startAnimate方法
|
||||||
|
this.show && !this.webviewHide ? this.startAnimate() : this.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
|
||||||
|
@import '@/uni_modules/uv-ui-tools/libs/css/color.scss';
|
||||||
|
$uv-loading-icon-color: #c8c9cc !default;
|
||||||
|
$uv-loading-icon-text-margin-left:4px !default;
|
||||||
|
$uv-loading-icon-text-color:$uv-content-color !default;
|
||||||
|
$uv-loading-icon-text-font-size:14px !default;
|
||||||
|
$uv-loading-icon-text-line-height:20px !default;
|
||||||
|
$uv-loading-width:30px !default;
|
||||||
|
$uv-loading-height:30px !default;
|
||||||
|
$uv-loading-max-width:100% !default;
|
||||||
|
$uv-loading-max-height:100% !default;
|
||||||
|
$uv-loading-semicircle-border-width: 2px !default;
|
||||||
|
$uv-loading-semicircle-border-color:transparent !default;
|
||||||
|
$uv-loading-semicircle-border-top-right-radius: 100px !default;
|
||||||
|
$uv-loading-semicircle-border-top-left-radius: 100px !default;
|
||||||
|
$uv-loading-semicircle-border-bottom-left-radius: 100px !default;
|
||||||
|
$uv-loading-semicircle-border-bottom-right-radiu: 100px !default;
|
||||||
|
$uv-loading-semicircle-border-style: solid !default;
|
||||||
|
$uv-loading-circle-border-top-right-radius: 100px !default;
|
||||||
|
$uv-loading-circle-border-top-left-radius: 100px !default;
|
||||||
|
$uv-loading-circle-border-bottom-left-radius: 100px !default;
|
||||||
|
$uv-loading-circle-border-bottom-right-radiu: 100px !default;
|
||||||
|
$uv-loading-circle-border-width:2px !default;
|
||||||
|
$uv-loading-circle-border-top-color:#e5e5e5 !default;
|
||||||
|
$uv-loading-circle-border-right-color:$uv-loading-circle-border-top-color !default;
|
||||||
|
$uv-loading-circle-border-bottom-color:$uv-loading-circle-border-top-color !default;
|
||||||
|
$uv-loading-circle-border-left-color:$uv-loading-circle-border-top-color !default;
|
||||||
|
$uv-loading-circle-border-style:solid !default;
|
||||||
|
$uv-loading-icon-host-font-size:0px !default;
|
||||||
|
$uv-loading-icon-host-line-height:1 !default;
|
||||||
|
$uv-loading-icon-vertical-margin:6px 0 0 !default;
|
||||||
|
$uv-loading-icon-dot-top:0 !default;
|
||||||
|
$uv-loading-icon-dot-left:0 !default;
|
||||||
|
$uv-loading-icon-dot-width:100% !default;
|
||||||
|
$uv-loading-icon-dot-height:100% !default;
|
||||||
|
$uv-loading-icon-dot-before-width:2px !default;
|
||||||
|
$uv-loading-icon-dot-before-height:25% !default;
|
||||||
|
$uv-loading-icon-dot-before-margin:0 auto !default;
|
||||||
|
$uv-loading-icon-dot-before-background-color:currentColor !default;
|
||||||
|
$uv-loading-icon-dot-before-border-radius:40% !default;
|
||||||
|
|
||||||
|
.uv-loading-icon {
|
||||||
|
/* #ifndef APP-NVUE */
|
||||||
|
// display: inline-flex;
|
||||||
|
/* #endif */
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: $uv-loading-icon-color;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
margin-left: $uv-loading-icon-text-margin-left;
|
||||||
|
color: $uv-loading-icon-text-color;
|
||||||
|
font-size: $uv-loading-icon-text-font-size;
|
||||||
|
line-height: $uv-loading-icon-text-line-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__spinner {
|
||||||
|
width: $uv-loading-width;
|
||||||
|
height: $uv-loading-height;
|
||||||
|
position: relative;
|
||||||
|
/* #ifndef APP-NVUE */
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: $uv-loading-max-width;
|
||||||
|
max-height: $uv-loading-max-height;
|
||||||
|
animation: uv-rotate 1s linear infinite;
|
||||||
|
/* #endif */
|
||||||
|
}
|
||||||
|
|
||||||
|
&__spinner--semicircle {
|
||||||
|
border-width: $uv-loading-semicircle-border-width;
|
||||||
|
border-color: $uv-loading-semicircle-border-color;
|
||||||
|
border-top-right-radius: $uv-loading-semicircle-border-top-right-radius;
|
||||||
|
border-top-left-radius: $uv-loading-semicircle-border-top-left-radius;
|
||||||
|
border-bottom-left-radius: $uv-loading-semicircle-border-bottom-left-radius;
|
||||||
|
border-bottom-right-radius: $uv-loading-semicircle-border-bottom-right-radiu;
|
||||||
|
border-style: $uv-loading-semicircle-border-style;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__spinner--circle {
|
||||||
|
border-top-right-radius: $uv-loading-circle-border-top-right-radius;
|
||||||
|
border-top-left-radius: $uv-loading-circle-border-top-left-radius;
|
||||||
|
border-bottom-left-radius: $uv-loading-circle-border-bottom-left-radius;
|
||||||
|
border-bottom-right-radius: $uv-loading-circle-border-bottom-right-radiu;
|
||||||
|
border-width: $uv-loading-circle-border-width;
|
||||||
|
border-top-color: $uv-loading-circle-border-top-color;
|
||||||
|
border-right-color: $uv-loading-circle-border-right-color;
|
||||||
|
border-bottom-color: $uv-loading-circle-border-bottom-color;
|
||||||
|
border-left-color: $uv-loading-circle-border-left-color;
|
||||||
|
border-style: $uv-loading-circle-border-style;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--vertical {
|
||||||
|
flex-direction: column
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #ifndef APP-NVUE */
|
||||||
|
:host {
|
||||||
|
font-size: $uv-loading-icon-host-font-size;
|
||||||
|
line-height: $uv-loading-icon-host-line-height;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uv-loading-icon {
|
||||||
|
&__spinner--spinner {
|
||||||
|
animation-timing-function: steps(12)
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text:empty {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
|
||||||
|
&--vertical &__text {
|
||||||
|
margin: $uv-loading-icon-vertical-margin;
|
||||||
|
color: $uv-content-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__dot {
|
||||||
|
position: absolute;
|
||||||
|
top: $uv-loading-icon-dot-top;
|
||||||
|
left: $uv-loading-icon-dot-left;
|
||||||
|
width: $uv-loading-icon-dot-width;
|
||||||
|
height: $uv-loading-icon-dot-height;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
display: block;
|
||||||
|
width: $uv-loading-icon-dot-before-width;
|
||||||
|
height: $uv-loading-icon-dot-before-height;
|
||||||
|
margin: $uv-loading-icon-dot-before-margin;
|
||||||
|
background-color: $uv-loading-icon-dot-before-background-color;
|
||||||
|
border-radius: $uv-loading-icon-dot-before-border-radius;
|
||||||
|
content: " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@for $i from 1 through 12 {
|
||||||
|
.uv-loading-icon__dot:nth-of-type(#{$i}) {
|
||||||
|
transform: rotate($i * 30deg);
|
||||||
|
opacity: 1 - 0.0625 * ($i - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes uv-rotate {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg)
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(1turn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #endif */
|
||||||
|
</style>
|
||||||
87
uni_modules/uv-loading-icon/package.json
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"id": "uv-loading-icon",
|
||||||
|
"displayName": "uv-loading-icon 加载动画 全面兼容vue3+2、app、h5、小程序等多端",
|
||||||
|
"version": "1.0.3",
|
||||||
|
"description": "此组件为一个小动画,目前用在uv-ui的uv-load-more加载更多等组件,还可以运用在项目中正在加载状态场景。",
|
||||||
|
"keywords": [
|
||||||
|
"uv-loading-icon",
|
||||||
|
"uvui",
|
||||||
|
"uv-ui",
|
||||||
|
"loading",
|
||||||
|
"加载动画"
|
||||||
|
],
|
||||||
|
"repository": "",
|
||||||
|
"engines": {
|
||||||
|
"HBuilderX": "^3.1.0"
|
||||||
|
},
|
||||||
|
"dcloudext": {
|
||||||
|
"type": "component-vue",
|
||||||
|
"sale": {
|
||||||
|
"regular": {
|
||||||
|
"price": "0.00"
|
||||||
|
},
|
||||||
|
"sourcecode": {
|
||||||
|
"price": "0.00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"qq": ""
|
||||||
|
},
|
||||||
|
"declaration": {
|
||||||
|
"ads": "无",
|
||||||
|
"data": "插件不采集任何数据",
|
||||||
|
"permissions": "无"
|
||||||
|
},
|
||||||
|
"npmurl": ""
|
||||||
|
},
|
||||||
|
"uni_modules": {
|
||||||
|
"dependencies": [
|
||||||
|
"uv-ui-tools"
|
||||||
|
],
|
||||||
|
"encrypt": [],
|
||||||
|
"platforms": {
|
||||||
|
"cloud": {
|
||||||
|
"tcb": "y",
|
||||||
|
"aliyun": "y"
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"Vue": {
|
||||||
|
"vue2": "y",
|
||||||
|
"vue3": "y"
|
||||||
|
},
|
||||||
|
"App": {
|
||||||
|
"app-vue": "y",
|
||||||
|
"app-nvue": "y"
|
||||||
|
},
|
||||||
|
"H5-mobile": {
|
||||||
|
"Safari": "y",
|
||||||
|
"Android Browser": "y",
|
||||||
|
"微信浏览器(Android)": "y",
|
||||||
|
"QQ浏览器(Android)": "y"
|
||||||
|
},
|
||||||
|
"H5-pc": {
|
||||||
|
"Chrome": "y",
|
||||||
|
"IE": "y",
|
||||||
|
"Edge": "y",
|
||||||
|
"Firefox": "y",
|
||||||
|
"Safari": "y"
|
||||||
|
},
|
||||||
|
"小程序": {
|
||||||
|
"微信": "y",
|
||||||
|
"阿里": "y",
|
||||||
|
"百度": "y",
|
||||||
|
"字节跳动": "y",
|
||||||
|
"QQ": "y",
|
||||||
|
"钉钉": "u",
|
||||||
|
"快手": "u",
|
||||||
|
"飞书": "u",
|
||||||
|
"京东": "u"
|
||||||
|
},
|
||||||
|
"快应用": {
|
||||||
|
"华为": "u",
|
||||||
|
"联盟": "u"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
uni_modules/uv-loading-icon/readme.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
## LoadingIcon 加载动画
|
||||||
|
|
||||||
|
> **组件名:uv-loading-icon**
|
||||||
|
|
||||||
|
此组件为一个小动画,目前用在 `uv-ui` 的 `uv-load-more` 加载更多等组件,还可以运用在项目中正在加载状态场景。
|
||||||
|
|
||||||
|
# <a href="https://www.uvui.cn/components/loadingIcon.html" target="_blank">查看文档</a>
|
||||||
|
|
||||||
|
## [下载完整示例项目](https://ext.dcloud.net.cn/plugin?name=uv-ui) <small>(请不要 下载插件ZIP)</small>
|
||||||
|
|
||||||
|
### [更多插件,请关注uv-ui组件库](https://ext.dcloud.net.cn/plugin?name=uv-ui)
|
||||||
|
|
||||||
|
<a href="https://ext.dcloud.net.cn/plugin?name=uv-ui" target="_blank">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
#### 如使用过程中有任何问题反馈,或者您对uv-ui有一些好的建议,欢迎加入uv-ui官方交流群:<a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a>
|
||||||
9
uni_modules/uv-overlay/changelog.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
## 1.0.3(2023-07-02)
|
||||||
|
uv-overlay 由于弹出层uv-transition的修改,组件内部做了相应的修改,参数不变。
|
||||||
|
## 1.0.2(2023-06-29)
|
||||||
|
1. 优化,H5端禁止穿透滚动
|
||||||
|
## 1.0.1(2023-05-16)
|
||||||
|
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
|
||||||
|
2. 优化部分功能
|
||||||
|
## 1.0.0(2023-05-10)
|
||||||
|
1. 新增uv-overlay组件
|
||||||
25
uni_modules/uv-overlay/components/uv-overlay/props.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
// 是否显示遮罩
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 层级z-index
|
||||||
|
zIndex: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 10070
|
||||||
|
},
|
||||||
|
// 遮罩的过渡时间,单位为ms
|
||||||
|
duration: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 300
|
||||||
|
},
|
||||||
|
// 不透明度值,当做rgba的第四个参数
|
||||||
|
opacity: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 0.5
|
||||||
|
},
|
||||||
|
...uni.$uv?.props?.overlay
|
||||||
|
}
|
||||||
|
}
|
||||||
85
uni_modules/uv-overlay/components/uv-overlay/uv-overlay.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<uv-transition
|
||||||
|
:show="show"
|
||||||
|
mode="fade"
|
||||||
|
custom-class="uv-overlay"
|
||||||
|
:duration="duration"
|
||||||
|
:custom-style="overlayStyle"
|
||||||
|
@click="clickHandler"
|
||||||
|
@touchmove.stop.prevent="clear"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</uv-transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
|
||||||
|
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
|
||||||
|
import props from './props.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* overlay 遮罩
|
||||||
|
* @description 创建一个遮罩层,用于强调特定的页面元素,并阻止用户对遮罩下层的内容进行操作,一般用于弹窗场景
|
||||||
|
* @tutorial https://www.uvui.cn/components/overlay.html
|
||||||
|
* @property {Boolean} show 是否显示遮罩(默认 false )
|
||||||
|
* @property {String | Number} zIndex zIndex 层级(默认 10070 )
|
||||||
|
* @property {String | Number} duration 动画时长,单位毫秒(默认 300 )
|
||||||
|
* @property {String | Number} opacity 不透明度值,当做rgba的第四个参数 (默认 0.5 )
|
||||||
|
* @property {Object} customStyle 定义需要用到的外部样式
|
||||||
|
* @event {Function} click 点击遮罩发送事件
|
||||||
|
* @example <uv-overlay :show="show" @click="show = false"></uv-overlay>
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: "uv-overlay",
|
||||||
|
emits: ['click'],
|
||||||
|
mixins: [mpMixin, mixin, props],
|
||||||
|
watch: {
|
||||||
|
show(newVal){
|
||||||
|
// #ifdef H5
|
||||||
|
if(newVal){
|
||||||
|
document.querySelector('body').style.overflow = 'hidden';
|
||||||
|
}else{
|
||||||
|
document.querySelector('body').style.overflow = '';
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
overlayStyle() {
|
||||||
|
const style = {
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
zIndex: this.zIndex,
|
||||||
|
bottom: 0,
|
||||||
|
'background-color': `rgba(0, 0, 0, ${this.opacity})`
|
||||||
|
}
|
||||||
|
return this.$uv.deepMerge(style, this.$uv.addStyle(this.customStyle))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickHandler() {
|
||||||
|
this.$emit('click')
|
||||||
|
},
|
||||||
|
clear() {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/* #ifndef APP-NVUE */
|
||||||
|
$uv-overlay-top:0 !default;
|
||||||
|
$uv-overlay-left:0 !default;
|
||||||
|
$uv-overlay-width:100% !default;
|
||||||
|
$uv-overlay-height:100% !default;
|
||||||
|
$uv-overlay-background-color:rgba(0, 0, 0, .7) !default;
|
||||||
|
.uv-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top:$uv-overlay-top;
|
||||||
|
left:$uv-overlay-left;
|
||||||
|
width: $uv-overlay-width;
|
||||||
|
height:$uv-overlay-height;
|
||||||
|
background-color:$uv-overlay-background-color;
|
||||||
|
}
|
||||||
|
/* #endif */
|
||||||
|
</style>
|
||||||
88
uni_modules/uv-overlay/package.json
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
{
|
||||||
|
"id": "uv-overlay",
|
||||||
|
"displayName": "uv-overlay 遮罩层 全面兼容小程序、nvue、vue2、vue3等多端",
|
||||||
|
"version": "1.0.3",
|
||||||
|
"description": "uv-overlay 创建一个遮罩层,用于强调特定的页面元素,并阻止用户对遮罩下层的内容进行操作,一般用于弹窗场景,uv-popup、uv-toast、uv-tooltip等组件就是用了该组件。",
|
||||||
|
"keywords": [
|
||||||
|
"uv-overlay",
|
||||||
|
"uvui",
|
||||||
|
"uv-ui",
|
||||||
|
"overlay",
|
||||||
|
"遮罩层"
|
||||||
|
],
|
||||||
|
"repository": "",
|
||||||
|
"engines": {
|
||||||
|
"HBuilderX": "^3.1.0"
|
||||||
|
},
|
||||||
|
"dcloudext": {
|
||||||
|
"type": "component-vue",
|
||||||
|
"sale": {
|
||||||
|
"regular": {
|
||||||
|
"price": "0.00"
|
||||||
|
},
|
||||||
|
"sourcecode": {
|
||||||
|
"price": "0.00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"qq": ""
|
||||||
|
},
|
||||||
|
"declaration": {
|
||||||
|
"ads": "无",
|
||||||
|
"data": "插件不采集任何数据",
|
||||||
|
"permissions": "无"
|
||||||
|
},
|
||||||
|
"npmurl": ""
|
||||||
|
},
|
||||||
|
"uni_modules": {
|
||||||
|
"dependencies": [
|
||||||
|
"uv-ui-tools",
|
||||||
|
"uv-transition"
|
||||||
|
],
|
||||||
|
"encrypt": [],
|
||||||
|
"platforms": {
|
||||||
|
"cloud": {
|
||||||
|
"tcb": "y",
|
||||||
|
"aliyun": "y"
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"Vue": {
|
||||||
|
"vue2": "y",
|
||||||
|
"vue3": "y"
|
||||||
|
},
|
||||||
|
"App": {
|
||||||
|
"app-vue": "y",
|
||||||
|
"app-nvue": "y"
|
||||||
|
},
|
||||||
|
"H5-mobile": {
|
||||||
|
"Safari": "y",
|
||||||
|
"Android Browser": "y",
|
||||||
|
"微信浏览器(Android)": "y",
|
||||||
|
"QQ浏览器(Android)": "y"
|
||||||
|
},
|
||||||
|
"H5-pc": {
|
||||||
|
"Chrome": "y",
|
||||||
|
"IE": "y",
|
||||||
|
"Edge": "y",
|
||||||
|
"Firefox": "y",
|
||||||
|
"Safari": "y"
|
||||||
|
},
|
||||||
|
"小程序": {
|
||||||
|
"微信": "y",
|
||||||
|
"阿里": "y",
|
||||||
|
"百度": "y",
|
||||||
|
"字节跳动": "y",
|
||||||
|
"QQ": "y",
|
||||||
|
"钉钉": "u",
|
||||||
|
"快手": "u",
|
||||||
|
"飞书": "u",
|
||||||
|
"京东": "u"
|
||||||
|
},
|
||||||
|
"快应用": {
|
||||||
|
"华为": "u",
|
||||||
|
"联盟": "u"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
uni_modules/uv-overlay/readme.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
## Overlay 遮罩层
|
||||||
|
|
||||||
|
> **组件名:uv-overlay**
|
||||||
|
|
||||||
|
创建一个遮罩层,用于强调特定的页面元素,并阻止用户对遮罩下层的内容进行操作,一般用于弹窗场景,uv-popup、uv-toast、uv-tooltip等组件就是用了该组件。
|
||||||
|
|
||||||
|
### <a href="https://www.uvui.cn/components/overlay.html" target="_blank">查看文档</a>
|
||||||
|
|
||||||
|
### [完整示例项目下载 | 关注更多组件](https://ext.dcloud.net.cn/plugin?name=uv-ui)
|
||||||
|
|
||||||
|
#### 如使用过程中有任何问题,或者您对uv-ui有一些好的建议,欢迎加入 uv-ui 交流群:<a href="https://ext.dcloud.net.cn/plugin?id=12287" target="_blank">uv-ui</a>、<a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a>
|
||||||
33
uni_modules/uv-picker/changelog.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
## 1.0.14(2023-12-29)
|
||||||
|
1. 修复上个版本引出的BUG
|
||||||
|
## 1.0.13(2023-12-26)
|
||||||
|
1. 修复抖音小程序滚到底不触发change的BUG
|
||||||
|
## 1.0.12(2023-11-20)
|
||||||
|
1. 修复issues反馈的问题uv-picker在组合式API的自定义组件中,columns动态赋值无法显示选项:https://gitee.com/climblee/uv-ui/issues/I8H0GQ
|
||||||
|
## 1.0.11(2023-10-11)
|
||||||
|
1. 将immediate-change默认值改为true,该值在于change回调的及时性,微信小程序生效
|
||||||
|
## 1.0.10(2023-08-25)
|
||||||
|
1. 增加round属性设置弹窗圆角,默认为0
|
||||||
|
## 1.0.9(2023-08-24)
|
||||||
|
1. 修复cli项目不返回值的问题
|
||||||
|
## 1.0.8(2023-08-04)
|
||||||
|
1. 优化
|
||||||
|
## 1.0.7(2023-08-02)
|
||||||
|
1. 改组件中删除uv-toolbar组件,请单独下载uv-toolbar组件
|
||||||
|
## 1.0.6(2023-07-02)
|
||||||
|
uv-picker 由于弹出层uv-popup的修改,打开和关闭方法更改,详情参考文档:https://www.uvui.cn/components/picker.html
|
||||||
|
## 1.0.5(2023-06-26)
|
||||||
|
1. 增加color参数
|
||||||
|
2. 增加activeColor参数
|
||||||
|
## 1.0.4(2023-06-15)
|
||||||
|
1. 修改支付宝报错的BUG
|
||||||
|
## 1.0.3(2023-06-12)
|
||||||
|
1. setColumnValues的使用统一化,避免某些平台报错
|
||||||
|
2. 取消change回调回传的组件实例,直接统一通过ref的方式调取setColumnValues方法
|
||||||
|
## 1.0.2(2023-05-23)
|
||||||
|
1. uv-toolbar组件新增下边框属性
|
||||||
|
## 1.0.1(2023-05-16)
|
||||||
|
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
|
||||||
|
2. 优化部分功能
|
||||||
|
## 1.0.0(2023-05-10)
|
||||||
|
uv-picker 选择器
|
||||||
95
uni_modules/uv-picker/components/uv-picker/props.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
// 是否展示顶部的操作栏
|
||||||
|
showToolbar: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 顶部标题
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 弹窗圆角
|
||||||
|
round: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
// 对象数组,设置每一列的数据
|
||||||
|
columns: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
// 是否显示加载中状态
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 各列中,单个选项的高度
|
||||||
|
itemHeight: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 44
|
||||||
|
},
|
||||||
|
// 取消按钮的文字
|
||||||
|
cancelText: {
|
||||||
|
type: String,
|
||||||
|
default: '取消'
|
||||||
|
},
|
||||||
|
// 确认按钮的文字
|
||||||
|
confirmText: {
|
||||||
|
type: String,
|
||||||
|
default: '确定'
|
||||||
|
},
|
||||||
|
// 取消按钮的颜色
|
||||||
|
cancelColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#909193'
|
||||||
|
},
|
||||||
|
// 确认按钮的颜色
|
||||||
|
confirmColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#3c9cff'
|
||||||
|
},
|
||||||
|
// 文字颜色
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 选中文字的颜色
|
||||||
|
activeColor: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 每列中可见选项的数量
|
||||||
|
visibleItemCount: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 5
|
||||||
|
},
|
||||||
|
// 选项对象中,需要展示的属性键名
|
||||||
|
keyName: {
|
||||||
|
type: String,
|
||||||
|
default: 'text'
|
||||||
|
},
|
||||||
|
// 是否允许点击遮罩关闭选择器
|
||||||
|
closeOnClickOverlay: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 是否允许点击确认关闭选择器
|
||||||
|
closeOnClickConfirm: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 各列的默认索引
|
||||||
|
defaultIndex: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
// 是否在手指松开时立即触发 change 事件。若不开启则会在滚动动画结束后触发 change 事件,只在微信2.21.1及以上有效
|
||||||
|
immediateChange: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
...uni.$uv?.props?.picker
|
||||||
|
}
|
||||||
|
}
|
||||||
330
uni_modules/uv-picker/components/uv-picker/uv-picker.vue
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
<template>
|
||||||
|
<uv-popup
|
||||||
|
ref="pickerPopup"
|
||||||
|
mode="bottom"
|
||||||
|
:round="round"
|
||||||
|
:close-on-click-overlay="closeOnClickOverlay"
|
||||||
|
@change="popupChange"
|
||||||
|
>
|
||||||
|
<view class="uv-picker">
|
||||||
|
<uv-toolbar
|
||||||
|
v-if="showToolbar"
|
||||||
|
:cancelColor="cancelColor"
|
||||||
|
:confirmColor="confirmColor"
|
||||||
|
:cancelText="cancelText"
|
||||||
|
:confirmText="confirmText"
|
||||||
|
:title="title"
|
||||||
|
@cancel="cancel"
|
||||||
|
@confirm="confirm"
|
||||||
|
></uv-toolbar>
|
||||||
|
<!-- #ifdef MP-TOUTIAO -->
|
||||||
|
<picker-view
|
||||||
|
class="uv-picker__view"
|
||||||
|
:indicatorStyle="`height: ${$uv.addUnit(itemHeight)}`"
|
||||||
|
:value="innerIndex"
|
||||||
|
:immediateChange="immediateChange"
|
||||||
|
:style="{
|
||||||
|
height: `${$uv.addUnit(visibleItemCount * itemHeight)}`
|
||||||
|
}"
|
||||||
|
@pickend="changeHandler"
|
||||||
|
>
|
||||||
|
<!-- #endif -->
|
||||||
|
<!-- #ifndef MP-TOUTIAO -->
|
||||||
|
<picker-view
|
||||||
|
class="uv-picker__view"
|
||||||
|
:indicatorStyle="`height: ${$uv.addUnit(itemHeight)}`"
|
||||||
|
:value="innerIndex"
|
||||||
|
:immediateChange="immediateChange"
|
||||||
|
:style="{
|
||||||
|
height: `${$uv.addUnit(visibleItemCount * itemHeight)}`
|
||||||
|
}"
|
||||||
|
@change="changeHandler"
|
||||||
|
>
|
||||||
|
<!-- #endif -->
|
||||||
|
<!-- @pickend在这里为了解决抖音等滚到底不触发change兼容性问题 -->
|
||||||
|
<picker-view-column
|
||||||
|
v-for="(item, index) in innerColumns"
|
||||||
|
:key="index"
|
||||||
|
class="uv-picker__view__column"
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
v-if="$uv.test.array(item)"
|
||||||
|
class="uv-picker__view__column__item uv-line-1"
|
||||||
|
v-for="(item1, index1) in item"
|
||||||
|
:key="index1"
|
||||||
|
:style="[{
|
||||||
|
height: $uv.addUnit(itemHeight),
|
||||||
|
lineHeight: $uv.addUnit(itemHeight),
|
||||||
|
fontWeight: index1 === innerIndex[index] ? 'bold' : 'normal'
|
||||||
|
},textStyle(index,index1)]"
|
||||||
|
>{{ getItemText(item1) }}</text>
|
||||||
|
</picker-view-column>
|
||||||
|
</picker-view>
|
||||||
|
<view
|
||||||
|
v-if="loading"
|
||||||
|
class="uv-picker--loading"
|
||||||
|
>
|
||||||
|
<uv-loading-icon mode="circle"></uv-loading-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</uv-popup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* uv-picker
|
||||||
|
* @description 选择器
|
||||||
|
* @property {Boolean} showToolbar 是否显示顶部的操作栏(默认 true )
|
||||||
|
* @property {String} title 顶部标题
|
||||||
|
* @property {Array} columns 对象数组,设置每一列的数据
|
||||||
|
* @property {Boolean} loading 是否显示加载中状态(默认 false )
|
||||||
|
* @property {String | Number} itemHeight 各列中,单个选项的高度(默认 44 )
|
||||||
|
* @property {String} cancelText 取消按钮的文字(默认 '取消' )
|
||||||
|
* @property {String} confirmText 确认按钮的文字(默认 '确定' )
|
||||||
|
* @property {String} cancelColor 取消按钮的颜色(默认 '#909193' )
|
||||||
|
* @property {String} confirmColor 确认按钮的颜色(默认 '#3c9cff' )
|
||||||
|
* @property {String} color 文字颜色(默认 '' )
|
||||||
|
* @property {String} activeColor 选中文字的颜色(默认 '' )
|
||||||
|
* @property {String | Number} visibleItemCount 每列中可见选项的数量(默认 5 )
|
||||||
|
* @property {String} keyName 选项对象中,需要展示的属性键名(默认 'text' )
|
||||||
|
* @property {Boolean} closeOnClickOverlay 是否允许点击遮罩关闭选择器(默认 false )
|
||||||
|
* @property {Array} defaultIndex 各列的默认索引
|
||||||
|
* @property {Boolean} immediateChange 是否在手指松开时立即触发change事件(默认 false )
|
||||||
|
* @event {Function} close 关闭选择器时触发
|
||||||
|
* @event {Function} cancel 点击取消按钮触发
|
||||||
|
* @event {Function} change 当选择值变化时触发
|
||||||
|
* @event {Function} confirm 点击确定按钮,返回当前选择的值
|
||||||
|
*/
|
||||||
|
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
|
||||||
|
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
|
||||||
|
import props from './props.js';
|
||||||
|
export default {
|
||||||
|
name: 'uv-picker',
|
||||||
|
emits: ['confirm','cancel','close','change'],
|
||||||
|
mixins: [mpMixin, mixin, props],
|
||||||
|
computed: {
|
||||||
|
// 为了解决支付宝不生效
|
||||||
|
textStyle(){
|
||||||
|
return (index,index1) => {
|
||||||
|
const style = {};
|
||||||
|
// #ifndef APP-NVUE
|
||||||
|
style.display = 'block';
|
||||||
|
// #endif
|
||||||
|
if(this.color) {
|
||||||
|
style.color = this.color;
|
||||||
|
}
|
||||||
|
if(this.activeColor && index1 === this.innerIndex[index]) {
|
||||||
|
style.color = this.activeColor;
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// 上一次选择的列索引
|
||||||
|
lastIndex: [],
|
||||||
|
// 索引值 ,对应picker-view的value
|
||||||
|
innerIndex: [],
|
||||||
|
// 各列的值
|
||||||
|
innerColumns: [],
|
||||||
|
// 上一次的变化列索引
|
||||||
|
columnIndex: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// 监听默认索引的变化,重新设置对应的值
|
||||||
|
defaultIndex: {
|
||||||
|
immediate: true,
|
||||||
|
handler(n) {
|
||||||
|
this.setIndexs(n, true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 监听columns参数的变化
|
||||||
|
columns: {
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
handler(n) {
|
||||||
|
this.setColumns(n)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
open() {
|
||||||
|
this.$refs.pickerPopup.open();
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.$refs.pickerPopup.close();
|
||||||
|
},
|
||||||
|
popupChange(e) {
|
||||||
|
if(!e.show) this.$emit('close');
|
||||||
|
},
|
||||||
|
// 获取item需要显示的文字,判别为对象还是文本
|
||||||
|
getItemText(item) {
|
||||||
|
if (this.$uv.test.object(item)) {
|
||||||
|
return item[this.keyName]
|
||||||
|
} else {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 点击工具栏的取消按钮
|
||||||
|
cancel() {
|
||||||
|
this.$emit('cancel');
|
||||||
|
this.close();
|
||||||
|
},
|
||||||
|
// 点击工具栏的确定按钮
|
||||||
|
confirm() {
|
||||||
|
// 在这里使用deepClone拷贝后,vue3会自动转换成原始对象,这样处理是因为cli项目可能出现不返回值的情况
|
||||||
|
this.$emit('confirm', this.$uv.deepClone({
|
||||||
|
indexs: this.innerIndex,
|
||||||
|
value: this.innerColumns.map((item, index) => item[this.innerIndex[index]]),
|
||||||
|
values: this.innerColumns
|
||||||
|
}));
|
||||||
|
if(this.closeOnClickConfirm) {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 选择器某一列的数据发生变化时触发
|
||||||
|
changeHandler(e) {
|
||||||
|
const {
|
||||||
|
value
|
||||||
|
} = e.detail
|
||||||
|
let index = 0,
|
||||||
|
columnIndex = 0
|
||||||
|
// 通过对比前后两次的列索引,得出当前变化的是哪一列
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
let item = value[i]
|
||||||
|
if (item !== (this.lastIndex[i] || 0)) { // 把undefined转为合法假值0
|
||||||
|
// 设置columnIndex为当前变化列的索引
|
||||||
|
columnIndex = i
|
||||||
|
// index则为变化列中的变化项的索引
|
||||||
|
index = item
|
||||||
|
break // 终止循环,即使少一次循环,也是性能的提升
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.columnIndex = columnIndex
|
||||||
|
const values = this.innerColumns
|
||||||
|
// 将当前的各项变化索引,设置为"上一次"的索引变化值
|
||||||
|
this.setLastIndex(value)
|
||||||
|
this.setIndexs(value)
|
||||||
|
|
||||||
|
this.$emit('change', {
|
||||||
|
value: this.innerColumns.map((item, index) => item[value[index]]),
|
||||||
|
index,
|
||||||
|
indexs: value,
|
||||||
|
// values为当前变化列的数组内容
|
||||||
|
values,
|
||||||
|
columnIndex
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 设置index索引,此方法可被外部调用设置
|
||||||
|
setIndexs(index, setLastIndex) {
|
||||||
|
this.innerIndex = this.$uv.deepClone(index)
|
||||||
|
if (setLastIndex) {
|
||||||
|
this.setLastIndex(index)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 记录上一次的各列索引位置
|
||||||
|
setLastIndex(index) {
|
||||||
|
// 当能进入此方法,意味着当前设置的各列默认索引,即为“上一次”的选中值,需要记录,是因为changeHandler中
|
||||||
|
// 需要拿前后的变化值进行对比,得出当前发生改变的是哪一列
|
||||||
|
this.lastIndex = this.$uv.deepClone(index)
|
||||||
|
},
|
||||||
|
// 设置对应列选项的所有值
|
||||||
|
setColumnValues(columnIndex, values) {
|
||||||
|
// 替换innerColumns数组中columnIndex索引的值为values,使用的是数组的splice方法
|
||||||
|
this.innerColumns.splice(columnIndex, 1, values)
|
||||||
|
// 拷贝一份原有的innerIndex做临时变量,将大于当前变化列的所有的列的默认索引设置为0
|
||||||
|
let tmpIndex = this.$uv.deepClone(this.innerIndex)
|
||||||
|
for (let i = 0; i < this.innerColumns.length; i++) {
|
||||||
|
if (i > this.columnIndex) {
|
||||||
|
tmpIndex[i] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 一次性赋值,不能单个修改,否则无效
|
||||||
|
this.setIndexs(tmpIndex)
|
||||||
|
},
|
||||||
|
// 获取对应列的所有选项
|
||||||
|
getColumnValues(columnIndex) {
|
||||||
|
// 进行同步阻塞,因为外部得到change事件之后,可能需要执行setColumnValues更新列的值
|
||||||
|
// 索引如果在外部change的回调中调用getColumnValues的话,可能无法得到变更后的列值,这里进行一定延时,保证值的准确性
|
||||||
|
(async () => {
|
||||||
|
await this.$uv.sleep()
|
||||||
|
})()
|
||||||
|
return this.innerColumns[columnIndex]
|
||||||
|
},
|
||||||
|
// 设置整体各列的columns的值
|
||||||
|
setColumns(columns) {
|
||||||
|
this.innerColumns = this.$uv.deepClone(columns)
|
||||||
|
// 如果在设置各列数据时,没有被设置默认的各列索引defaultIndex,那么用0去填充它,数组长度为列的数量
|
||||||
|
if (this.innerIndex.length === 0) {
|
||||||
|
this.innerIndex = new Array(columns.length).fill(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 获取各列选中值对应的索引
|
||||||
|
getIndexs() {
|
||||||
|
return this.innerIndex
|
||||||
|
},
|
||||||
|
// 获取各列选中的值
|
||||||
|
getValues() {
|
||||||
|
// 进行同步阻塞,因为外部得到change事件之后,可能需要执行setColumnValues更新列的值
|
||||||
|
// 索引如果在外部change的回调中调用getValues的话,可能无法得到变更后的列值,这里进行一定延时,保证值的准确性
|
||||||
|
(async () => {
|
||||||
|
await this.$uv.sleep()
|
||||||
|
})()
|
||||||
|
return this.innerColumns.map((item, index) => item[this.innerIndex[index]])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
$show-lines: 1;
|
||||||
|
@import '@/uni_modules/uv-ui-tools/libs/css/variable.scss';
|
||||||
|
@import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
|
||||||
|
@import '@/uni_modules/uv-ui-tools/libs/css/color.scss';
|
||||||
|
.uv-picker {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&__view {
|
||||||
|
|
||||||
|
&__column {
|
||||||
|
@include flex;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
@include flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 16px;
|
||||||
|
text-align: center;
|
||||||
|
/* #ifndef APP-NVUE */
|
||||||
|
display: block;
|
||||||
|
/* #endif */
|
||||||
|
color: $uv-main-color;
|
||||||
|
|
||||||
|
&--disabled {
|
||||||
|
/* #ifndef APP-NVUE */
|
||||||
|
cursor: not-allowed;
|
||||||
|
/* #endif */
|
||||||
|
opacity: 0.35;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--loading {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
@include flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(255, 255, 255, 0.87);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
90
uni_modules/uv-picker/package.json
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"id": "uv-picker",
|
||||||
|
"displayName": "uv-picker 选择器 全面兼容vue3+2、app、h5、小程序等多端",
|
||||||
|
"version": "1.0.14",
|
||||||
|
"description": "uv-picker 此选择器用于单列,多列,多列联动的选择场景...",
|
||||||
|
"keywords": [
|
||||||
|
"uv-picker",
|
||||||
|
"uvui",
|
||||||
|
"uv-ui",
|
||||||
|
"picker",
|
||||||
|
"联动选择"
|
||||||
|
],
|
||||||
|
"repository": "",
|
||||||
|
"engines": {
|
||||||
|
"HBuilderX": "^3.1.0"
|
||||||
|
},
|
||||||
|
"dcloudext": {
|
||||||
|
"type": "component-vue",
|
||||||
|
"sale": {
|
||||||
|
"regular": {
|
||||||
|
"price": "0.00"
|
||||||
|
},
|
||||||
|
"sourcecode": {
|
||||||
|
"price": "0.00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"qq": ""
|
||||||
|
},
|
||||||
|
"declaration": {
|
||||||
|
"ads": "无",
|
||||||
|
"data": "插件不采集任何数据",
|
||||||
|
"permissions": "无"
|
||||||
|
},
|
||||||
|
"npmurl": ""
|
||||||
|
},
|
||||||
|
"uni_modules": {
|
||||||
|
"dependencies": [
|
||||||
|
"uv-ui-tools",
|
||||||
|
"uv-popup",
|
||||||
|
"uv-loading-icon",
|
||||||
|
"uv-toolbar"
|
||||||
|
],
|
||||||
|
"encrypt": [],
|
||||||
|
"platforms": {
|
||||||
|
"cloud": {
|
||||||
|
"tcb": "y",
|
||||||
|
"aliyun": "y"
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"Vue": {
|
||||||
|
"vue2": "y",
|
||||||
|
"vue3": "y"
|
||||||
|
},
|
||||||
|
"App": {
|
||||||
|
"app-vue": "y",
|
||||||
|
"app-nvue": "y"
|
||||||
|
},
|
||||||
|
"H5-mobile": {
|
||||||
|
"Safari": "y",
|
||||||
|
"Android Browser": "y",
|
||||||
|
"微信浏览器(Android)": "y",
|
||||||
|
"QQ浏览器(Android)": "y"
|
||||||
|
},
|
||||||
|
"H5-pc": {
|
||||||
|
"Chrome": "y",
|
||||||
|
"IE": "y",
|
||||||
|
"Edge": "y",
|
||||||
|
"Firefox": "y",
|
||||||
|
"Safari": "y"
|
||||||
|
},
|
||||||
|
"小程序": {
|
||||||
|
"微信": "y",
|
||||||
|
"阿里": "y",
|
||||||
|
"百度": "y",
|
||||||
|
"字节跳动": "y",
|
||||||
|
"QQ": "y",
|
||||||
|
"钉钉": "u",
|
||||||
|
"快手": "u",
|
||||||
|
"飞书": "u",
|
||||||
|
"京东": "u"
|
||||||
|
},
|
||||||
|
"快应用": {
|
||||||
|
"华为": "u",
|
||||||
|
"联盟": "u"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
uni_modules/uv-picker/readme.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
## Picker 选择器
|
||||||
|
|
||||||
|
> **组件名:uv-picker**
|
||||||
|
|
||||||
|
此选择器用于单列,多列,多列联动的选择场景。
|
||||||
|
|
||||||
|
`uv-datetime-picker`等组件也用到了该组件,功能完善,需要特别注意的是`columns`参数的形式是数组嵌套。
|
||||||
|
|
||||||
|
# <a href="https://www.uvui.cn/components/picker.html" target="_blank">查看文档</a>
|
||||||
|
|
||||||
|
## [下载完整示例项目](https://ext.dcloud.net.cn/plugin?name=uv-ui) <small>(请不要 下载插件ZIP)</small>
|
||||||
|
|
||||||
|
### [更多插件,请关注uv-ui组件库](https://ext.dcloud.net.cn/plugin?name=uv-ui)
|
||||||
|
|
||||||
|
<a href="https://ext.dcloud.net.cn/plugin?name=uv-ui" target="_blank">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
#### 如使用过程中有任何问题反馈,或者您对uv-ui有一些好的建议,欢迎加入uv-ui官方交流群:<a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a>
|
||||||
18
uni_modules/uv-popup/changelog.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
## 1.0.7(2023-11-20)
|
||||||
|
修复issues问题:https://gitee.com/climblee/uv-ui/issues/I8HDLO
|
||||||
|
## 1.0.6(2023-10-13)
|
||||||
|
1. 优化vue,内容有背景色,设置圆角被遮挡的情况
|
||||||
|
## 1.0.5(2023-09-10)
|
||||||
|
1. 修复H5默认层级过高的问题
|
||||||
|
2. 修复全局设置prop无效的问题
|
||||||
|
## 1.0.4(2023-08-08)
|
||||||
|
1. 修复修改zIndex不生效的BUG
|
||||||
|
## 1.0.3(2023-07-02)
|
||||||
|
uv-popup 弹出层,代码重构优化,性能翻倍,小程序体验性能更加,避免卡顿。打开和关闭方法更改,详情参考文档:https://www.uvui.cn/components/popup.html
|
||||||
|
## 1.0.2(2023-06-11)
|
||||||
|
1. 修复zIndex层级问题
|
||||||
|
## 1.0.1(2023-05-16)
|
||||||
|
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
|
||||||
|
2. 优化部分功能
|
||||||
|
## 1.0.0(2023-05-10)
|
||||||
|
1. 新增uv-popup组件
|
||||||
45
uni_modules/uv-popup/components/uv-popup/keypress.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// #ifdef H5
|
||||||
|
export default {
|
||||||
|
name: 'Keypress',
|
||||||
|
props: {
|
||||||
|
disable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
const keyNames = {
|
||||||
|
esc: ['Esc', 'Escape'],
|
||||||
|
tab: 'Tab',
|
||||||
|
enter: 'Enter',
|
||||||
|
space: [' ', 'Spacebar'],
|
||||||
|
up: ['Up', 'ArrowUp'],
|
||||||
|
left: ['Left', 'ArrowLeft'],
|
||||||
|
right: ['Right', 'ArrowRight'],
|
||||||
|
down: ['Down', 'ArrowDown'],
|
||||||
|
delete: ['Backspace', 'Delete', 'Del']
|
||||||
|
}
|
||||||
|
const listener = ($event) => {
|
||||||
|
if (this.disable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const keyName = Object.keys(keyNames).find(key => {
|
||||||
|
const keyName = $event.key
|
||||||
|
const value = keyNames[key]
|
||||||
|
return value === keyName || (Array.isArray(value) && value.includes(keyName))
|
||||||
|
})
|
||||||
|
if (keyName) {
|
||||||
|
// 避免和其他按键事件冲突
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$emit(keyName, {})
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('keyup', listener)
|
||||||
|
// this.$once('hook:beforeDestroy', () => {
|
||||||
|
// document.removeEventListener('keyup', listener)
|
||||||
|
// })
|
||||||
|
},
|
||||||
|
render: () => {}
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
539
uni_modules/uv-popup/components/uv-popup/uv-popup.vue
Normal file
@@ -0,0 +1,539 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
v-if="showPopup"
|
||||||
|
class="uv-popup"
|
||||||
|
:class="[popupClass, isDesktop ? 'fixforpc-z-index' : '']"
|
||||||
|
:style="[{zIndex: zIndex}]"
|
||||||
|
>
|
||||||
|
<view @touchstart="touchstart">
|
||||||
|
<!-- 遮罩层 -->
|
||||||
|
<uv-overlay
|
||||||
|
key="1"
|
||||||
|
v-if="maskShow && overlay"
|
||||||
|
:show="showTrans"
|
||||||
|
:duration="duration"
|
||||||
|
:custom-style="overlayStyle"
|
||||||
|
:opacity="overlayOpacity"
|
||||||
|
:zIndex="zIndex"
|
||||||
|
@click="onTap"
|
||||||
|
></uv-overlay>
|
||||||
|
<uv-transition
|
||||||
|
key="2"
|
||||||
|
:mode="ani"
|
||||||
|
name="content"
|
||||||
|
:custom-style="transitionStyle"
|
||||||
|
:duration="duration"
|
||||||
|
:show="showTrans"
|
||||||
|
@click="onTap"
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
class="uv-popup__content"
|
||||||
|
:style="[contentStyle]"
|
||||||
|
:class="[popupClass]"
|
||||||
|
@click="clear"
|
||||||
|
>
|
||||||
|
<uv-status-bar v-if="safeAreaInsetTop"></uv-status-bar>
|
||||||
|
<slot />
|
||||||
|
<uv-safe-bottom v-if="safeAreaInsetBottom"></uv-safe-bottom>
|
||||||
|
<view
|
||||||
|
v-if="closeable"
|
||||||
|
@tap.stop="close"
|
||||||
|
class="uv-popup__content__close"
|
||||||
|
:class="['uv-popup__content__close--' + closeIconPos]"
|
||||||
|
hover-class="uv-popup__content__close--hover"
|
||||||
|
hover-stay-time="150"
|
||||||
|
>
|
||||||
|
<uv-icon
|
||||||
|
name="close"
|
||||||
|
color="#909399"
|
||||||
|
size="18"
|
||||||
|
bold
|
||||||
|
></uv-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</uv-transition>
|
||||||
|
</view>
|
||||||
|
<!-- #ifdef H5 -->
|
||||||
|
<keypress v-if="maskShow" @esc="onTap" />
|
||||||
|
<!-- #endif -->
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// #ifdef H5
|
||||||
|
import keypress from './keypress.js'
|
||||||
|
// #endif
|
||||||
|
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
|
||||||
|
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
|
||||||
|
/**
|
||||||
|
* PopUp 弹出层
|
||||||
|
* @description 弹出层组件,为了解决遮罩弹层的问题
|
||||||
|
* @tutorial https://www.uvui.cn/components/popup.html
|
||||||
|
* @property {String} mode = [top|center|bottom|left|right] 弹出方式
|
||||||
|
* @value top 顶部弹出
|
||||||
|
* @value center 中间弹出
|
||||||
|
* @value bottom 底部弹出
|
||||||
|
* @value left 左侧弹出
|
||||||
|
* @value right 右侧弹出
|
||||||
|
* @property {Number} duration 动画时长,默认300
|
||||||
|
* @property {Boolean} overlay 是否显示遮罩,默认true
|
||||||
|
* @property {Boolean} overlayOpacity 遮罩透明度,默认0.5
|
||||||
|
* @property {Object} overlayStyle 遮罩自定义样式
|
||||||
|
* @property {Boolean} closeOnClickOverlay = [true|false] 蒙版点击是否关闭弹窗,默认true
|
||||||
|
* @property {Number | String} zIndex 弹出层的层级
|
||||||
|
* @property {Boolean} safeAreaInsetTop 是否留出顶部安全区(状态栏高度),默认false
|
||||||
|
* @property {Boolean} safeAreaInsetBottom 是否为留出底部安全区适配,默认true
|
||||||
|
* @property {Boolean} closeable 是否显示关闭图标,默认false
|
||||||
|
* @property {Boolean} closeIconPos 自定义关闭图标位置,`top-left`-左上角,`top-right`-右上角,`bottom-left`-左下角,`bottom-right`-右下角,默认top-right
|
||||||
|
* @property {String} bgColor 主窗口背景色
|
||||||
|
* @property {String} maskBackgroundColor 蒙版颜色
|
||||||
|
* @property {Boolean} customStyle 自定义样式
|
||||||
|
* @event {Function} change 打开关闭弹窗触发,e={show: false}
|
||||||
|
* @event {Function} maskClick 点击遮罩触发
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: 'uv-popup',
|
||||||
|
components: {
|
||||||
|
// #ifdef H5
|
||||||
|
keypress
|
||||||
|
// #endif
|
||||||
|
},
|
||||||
|
mixins: [mpMixin, mixin],
|
||||||
|
emits: ['change', 'maskClick'],
|
||||||
|
props: {
|
||||||
|
// 弹出层类型,可选值,top: 顶部弹出层;bottom:底部弹出层;center:全屏弹出层
|
||||||
|
// message: 消息提示 ; dialog : 对话框
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: 'center'
|
||||||
|
},
|
||||||
|
// 动画时长,单位ms
|
||||||
|
duration: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 300
|
||||||
|
},
|
||||||
|
// 层级
|
||||||
|
zIndex: {
|
||||||
|
type: [String, Number],
|
||||||
|
// #ifdef H5
|
||||||
|
default: 997
|
||||||
|
// #endif
|
||||||
|
// #ifndef H5
|
||||||
|
default: 10075
|
||||||
|
// #endif
|
||||||
|
},
|
||||||
|
bgColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#ffffff'
|
||||||
|
},
|
||||||
|
safeArea: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 是否显示遮罩
|
||||||
|
overlay: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 点击遮罩是否关闭弹窗
|
||||||
|
closeOnClickOverlay: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 遮罩的透明度,0-1之间
|
||||||
|
overlayOpacity: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 0.4
|
||||||
|
},
|
||||||
|
// 自定义遮罩的样式
|
||||||
|
overlayStyle: {
|
||||||
|
type: [Object, String],
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 是否为iPhoneX留出底部安全距离
|
||||||
|
safeAreaInsetBottom: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 是否留出顶部安全距离(状态栏高度)
|
||||||
|
safeAreaInsetTop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 是否显示关闭图标
|
||||||
|
closeable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 自定义关闭图标位置,top-left为左上角,top-right为右上角,bottom-left为左下角,bottom-right为右下角
|
||||||
|
closeIconPos: {
|
||||||
|
type: String,
|
||||||
|
default: 'top-right'
|
||||||
|
},
|
||||||
|
// mode=center,也即中部弹出时,是否使用缩放模式
|
||||||
|
zoom: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
round: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
...uni.$uv?.props?.popup
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
/**
|
||||||
|
* 监听type类型
|
||||||
|
*/
|
||||||
|
type: {
|
||||||
|
handler: function(type) {
|
||||||
|
if (!this.config[type]) return
|
||||||
|
this[this.config[type]](true)
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
},
|
||||||
|
isDesktop: {
|
||||||
|
handler: function(newVal) {
|
||||||
|
if (!this.config[newVal]) return
|
||||||
|
this[this.config[this.mode]](true)
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
},
|
||||||
|
// H5 下禁止底部滚动
|
||||||
|
showPopup(show) {
|
||||||
|
// #ifdef H5
|
||||||
|
// fix by mehaotian 处理 h5 滚动穿透的问题
|
||||||
|
document.getElementsByTagName('body')[0].style.overflow = show ? 'hidden' : 'visible'
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
ani: [],
|
||||||
|
showPopup: false,
|
||||||
|
showTrans: false,
|
||||||
|
popupWidth: 0,
|
||||||
|
popupHeight: 0,
|
||||||
|
config: {
|
||||||
|
top: 'top',
|
||||||
|
bottom: 'bottom',
|
||||||
|
center: 'center',
|
||||||
|
left: 'left',
|
||||||
|
right: 'right',
|
||||||
|
message: 'top',
|
||||||
|
dialog: 'center',
|
||||||
|
share: 'bottom'
|
||||||
|
},
|
||||||
|
transitionStyle: {
|
||||||
|
position: 'fixed',
|
||||||
|
left: 0,
|
||||||
|
right: 0
|
||||||
|
},
|
||||||
|
maskShow: true,
|
||||||
|
mkclick: true,
|
||||||
|
popupClass: this.isDesktop ? 'fixforpc-top' : 'top',
|
||||||
|
direction: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isDesktop() {
|
||||||
|
return this.popupWidth >= 500 && this.popupHeight >= 500
|
||||||
|
},
|
||||||
|
bg() {
|
||||||
|
if (this.bgColor === '' || this.bgColor === 'none' || this.$uv.getPx(this.round)>0) {
|
||||||
|
return 'transparent'
|
||||||
|
}
|
||||||
|
return this.bgColor
|
||||||
|
},
|
||||||
|
contentStyle() {
|
||||||
|
const style = {};
|
||||||
|
if (this.bgColor) {
|
||||||
|
style.backgroundColor = this.bg
|
||||||
|
}
|
||||||
|
if(this.round) {
|
||||||
|
const value = this.$uv.addUnit(this.round)
|
||||||
|
const mode = this.direction?this.direction:this.mode
|
||||||
|
style.backgroundColor = this.bgColor
|
||||||
|
if(mode === 'top') {
|
||||||
|
style.borderBottomLeftRadius = value
|
||||||
|
style.borderBottomRightRadius = value
|
||||||
|
} else if(mode === 'bottom') {
|
||||||
|
style.borderTopLeftRadius = value
|
||||||
|
style.borderTopRightRadius = value
|
||||||
|
} else if(mode === 'center') {
|
||||||
|
style.borderRadius = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.$uv.deepMerge(style, this.$uv.addStyle(this.customStyle))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// #ifndef VUE3
|
||||||
|
// TODO vue2
|
||||||
|
destroyed() {
|
||||||
|
this.setH5Visible()
|
||||||
|
},
|
||||||
|
// #endif
|
||||||
|
// #ifdef VUE3
|
||||||
|
// TODO vue3
|
||||||
|
unmounted() {
|
||||||
|
this.setH5Visible()
|
||||||
|
},
|
||||||
|
// #endif
|
||||||
|
created() {
|
||||||
|
// TODO 处理 message 组件生命周期异常的问题
|
||||||
|
this.messageChild = null
|
||||||
|
// TODO 解决头条冒泡的问题
|
||||||
|
this.clearPropagation = false
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setH5Visible() {
|
||||||
|
// #ifdef H5
|
||||||
|
// fix by mehaotian 处理 h5 滚动穿透的问题
|
||||||
|
document.getElementsByTagName('body')[0].style.overflow = 'visible'
|
||||||
|
// #endif
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 公用方法,不显示遮罩层
|
||||||
|
*/
|
||||||
|
closeMask() {
|
||||||
|
this.maskShow = false
|
||||||
|
},
|
||||||
|
// TODO nvue 取消冒泡
|
||||||
|
clear(e) {
|
||||||
|
// #ifndef APP-NVUE
|
||||||
|
e.stopPropagation()
|
||||||
|
// #endif
|
||||||
|
this.clearPropagation = true
|
||||||
|
},
|
||||||
|
|
||||||
|
open(direction) {
|
||||||
|
// fix by mehaotian 处理快速打开关闭的情况
|
||||||
|
if (this.showPopup) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let innerType = ['top', 'center', 'bottom', 'left', 'right', 'message', 'dialog', 'share']
|
||||||
|
if (!(direction && innerType.indexOf(direction) !== -1)) {
|
||||||
|
direction = this.mode
|
||||||
|
}else {
|
||||||
|
this.direction = direction;
|
||||||
|
}
|
||||||
|
if (!this.config[direction]) {
|
||||||
|
return this.$uv.error(`缺少类型:${direction}`);
|
||||||
|
}
|
||||||
|
this[this.config[direction]]()
|
||||||
|
this.$emit('change', {
|
||||||
|
show: true,
|
||||||
|
type: direction
|
||||||
|
})
|
||||||
|
},
|
||||||
|
close(type) {
|
||||||
|
this.showTrans = false
|
||||||
|
this.$emit('change', {
|
||||||
|
show: false,
|
||||||
|
type: this.mode
|
||||||
|
})
|
||||||
|
clearTimeout(this.timer)
|
||||||
|
// // 自定义关闭事件
|
||||||
|
this.timer = setTimeout(() => {
|
||||||
|
this.showPopup = false
|
||||||
|
}, 300)
|
||||||
|
},
|
||||||
|
// TODO 处理冒泡事件,头条的冒泡事件有问题 ,先这样兼容
|
||||||
|
touchstart() {
|
||||||
|
this.clearPropagation = false
|
||||||
|
},
|
||||||
|
onTap() {
|
||||||
|
if (this.clearPropagation) {
|
||||||
|
// fix by mehaotian 兼容 nvue
|
||||||
|
this.clearPropagation = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$emit('maskClick')
|
||||||
|
if (!this.closeOnClickOverlay) return
|
||||||
|
this.close()
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 顶部弹出样式处理
|
||||||
|
*/
|
||||||
|
top(type) {
|
||||||
|
this.popupClass = this.isDesktop ? 'fixforpc-top' : 'top'
|
||||||
|
this.ani = ['slide-top']
|
||||||
|
this.transitionStyle = {
|
||||||
|
position: 'fixed',
|
||||||
|
zIndex: this.zIndex,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
backgroundColor: this.bg
|
||||||
|
}
|
||||||
|
// TODO 兼容 type 属性 ,后续会废弃
|
||||||
|
if (type) return
|
||||||
|
this.showPopup = true
|
||||||
|
this.showTrans = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.messageChild && this.mode === 'message') {
|
||||||
|
this.messageChild.timerClose()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 底部弹出样式处理
|
||||||
|
*/
|
||||||
|
bottom(type) {
|
||||||
|
this.popupClass = 'bottom'
|
||||||
|
this.ani = ['slide-bottom']
|
||||||
|
this.transitionStyle = {
|
||||||
|
position: 'fixed',
|
||||||
|
zIndex: this.zIndex,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: this.bg
|
||||||
|
}
|
||||||
|
// TODO 兼容 type 属性 ,后续会废弃
|
||||||
|
if (type) return
|
||||||
|
this.showPopup = true
|
||||||
|
this.showTrans = true
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 中间弹出样式处理
|
||||||
|
*/
|
||||||
|
center(type) {
|
||||||
|
this.popupClass = 'center'
|
||||||
|
this.ani = this.zoom?['zoom-in', 'fade']:['fade'];
|
||||||
|
this.transitionStyle = {
|
||||||
|
position: 'fixed',
|
||||||
|
zIndex: this.zIndex,
|
||||||
|
/* #ifndef APP-NVUE */
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
/* #endif */
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center'
|
||||||
|
}
|
||||||
|
// TODO 兼容 type 属性 ,后续会废弃
|
||||||
|
if (type) return
|
||||||
|
this.showPopup = true
|
||||||
|
this.showTrans = true
|
||||||
|
},
|
||||||
|
left(type) {
|
||||||
|
this.popupClass = 'left'
|
||||||
|
this.ani = ['slide-left']
|
||||||
|
this.transitionStyle = {
|
||||||
|
position: 'fixed',
|
||||||
|
zIndex: this.zIndex,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
top: 0,
|
||||||
|
backgroundColor: this.bg,
|
||||||
|
/* #ifndef APP-NVUE */
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
/* #endif */
|
||||||
|
}
|
||||||
|
// TODO 兼容 type 属性 ,后续会废弃
|
||||||
|
if (type) return
|
||||||
|
this.showPopup = true
|
||||||
|
this.showTrans = true
|
||||||
|
},
|
||||||
|
right(type) {
|
||||||
|
this.popupClass = 'right'
|
||||||
|
this.ani = ['slide-right']
|
||||||
|
this.transitionStyle = {
|
||||||
|
position: 'fixed',
|
||||||
|
zIndex: this.zIndex,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
backgroundColor: this.bg,
|
||||||
|
/* #ifndef APP-NVUE */
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
/* #endif */
|
||||||
|
}
|
||||||
|
// TODO 兼容 type 属性 ,后续会废弃
|
||||||
|
if (type) return
|
||||||
|
this.showPopup = true
|
||||||
|
this.showTrans = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.uv-popup {
|
||||||
|
position: fixed;
|
||||||
|
/* #ifndef APP-NVUE */
|
||||||
|
z-index: 99;
|
||||||
|
|
||||||
|
/* #endif */
|
||||||
|
&.top,
|
||||||
|
&.left,
|
||||||
|
&.right {
|
||||||
|
/* #ifdef H5 */
|
||||||
|
top: var(--window-top);
|
||||||
|
/* #endif */
|
||||||
|
/* #ifndef H5 */
|
||||||
|
top: 0;
|
||||||
|
/* #endif */
|
||||||
|
}
|
||||||
|
|
||||||
|
.uv-popup__content {
|
||||||
|
/* #ifndef APP-NVUE */
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
/* #endif */
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.left,
|
||||||
|
&.right {
|
||||||
|
/* #ifdef H5 */
|
||||||
|
padding-top: var(--window-top);
|
||||||
|
/* #endif */
|
||||||
|
/* #ifndef H5 */
|
||||||
|
padding-top: 0;
|
||||||
|
/* #endif */
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
&__close {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
&--hover {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__close--top-left {
|
||||||
|
top: 15px;
|
||||||
|
left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__close--top-right {
|
||||||
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__close--bottom-left {
|
||||||
|
bottom: 15px;
|
||||||
|
left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__close--bottom-right {
|
||||||
|
right: 15px;
|
||||||
|
bottom: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixforpc-z-index {
|
||||||
|
/* #ifndef APP-NVUE */
|
||||||
|
z-index: 999;
|
||||||
|
/* #endif */
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixforpc-top {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
92
uni_modules/uv-popup/package.json
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
"id": "uv-popup",
|
||||||
|
"displayName": "uv-popup 弹出层 全面兼容vue3+2、app、h5、小程序等多端",
|
||||||
|
"version": "1.0.7",
|
||||||
|
"description": "uv-popup 弹出层容器,用于展示弹窗、信息提示等内容,支持上、下、左、右和中部弹出。组件只提供容器,内部内容由用户自定义。",
|
||||||
|
"keywords": [
|
||||||
|
"uv-popup",
|
||||||
|
"uvui",
|
||||||
|
"uv-ui",
|
||||||
|
"popup",
|
||||||
|
"弹出层"
|
||||||
|
],
|
||||||
|
"repository": "",
|
||||||
|
"engines": {
|
||||||
|
"HBuilderX": "^3.1.0"
|
||||||
|
},
|
||||||
|
"dcloudext": {
|
||||||
|
"type": "component-vue",
|
||||||
|
"sale": {
|
||||||
|
"regular": {
|
||||||
|
"price": "0.00"
|
||||||
|
},
|
||||||
|
"sourcecode": {
|
||||||
|
"price": "0.00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"qq": ""
|
||||||
|
},
|
||||||
|
"declaration": {
|
||||||
|
"ads": "无",
|
||||||
|
"data": "插件不采集任何数据",
|
||||||
|
"permissions": "无"
|
||||||
|
},
|
||||||
|
"npmurl": ""
|
||||||
|
},
|
||||||
|
"uni_modules": {
|
||||||
|
"dependencies": [
|
||||||
|
"uv-ui-tools",
|
||||||
|
"uv-overlay",
|
||||||
|
"uv-transition",
|
||||||
|
"uv-icon",
|
||||||
|
"uv-status-bar",
|
||||||
|
"uv-safe-bottom"
|
||||||
|
],
|
||||||
|
"encrypt": [],
|
||||||
|
"platforms": {
|
||||||
|
"cloud": {
|
||||||
|
"tcb": "y",
|
||||||
|
"aliyun": "y"
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"Vue": {
|
||||||
|
"vue2": "y",
|
||||||
|
"vue3": "y"
|
||||||
|
},
|
||||||
|
"App": {
|
||||||
|
"app-vue": "y",
|
||||||
|
"app-nvue": "y"
|
||||||
|
},
|
||||||
|
"H5-mobile": {
|
||||||
|
"Safari": "y",
|
||||||
|
"Android Browser": "y",
|
||||||
|
"微信浏览器(Android)": "y",
|
||||||
|
"QQ浏览器(Android)": "y"
|
||||||
|
},
|
||||||
|
"H5-pc": {
|
||||||
|
"Chrome": "y",
|
||||||
|
"IE": "y",
|
||||||
|
"Edge": "y",
|
||||||
|
"Firefox": "y",
|
||||||
|
"Safari": "y"
|
||||||
|
},
|
||||||
|
"小程序": {
|
||||||
|
"微信": "y",
|
||||||
|
"阿里": "y",
|
||||||
|
"百度": "y",
|
||||||
|
"字节跳动": "y",
|
||||||
|
"QQ": "y",
|
||||||
|
"钉钉": "u",
|
||||||
|
"快手": "u",
|
||||||
|
"飞书": "u",
|
||||||
|
"京东": "u"
|
||||||
|
},
|
||||||
|
"快应用": {
|
||||||
|
"华为": "u",
|
||||||
|
"联盟": "u"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
uni_modules/uv-popup/readme.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
## Popup 弹出层
|
||||||
|
|
||||||
|
> **组件名:uv-popup**
|
||||||
|
|
||||||
|
弹出层容器,用于展示弹窗、信息提示等内容,支持上、下、左、右和中部弹出。组件只提供容器,内部内容由用户自定义。
|
||||||
|
|
||||||
|
该组件已经放弃原来`uview2.x`的写法,参照了官方`uni-popup`的写法进行重构。在小程序端的性能大大提升,打开和关闭避免延迟,调用方法与之前相比也有所差异,具体请查看文档。
|
||||||
|
|
||||||
|
# <a href="https://www.uvui.cn/components/popup.html" target="_blank">查看文档</a>
|
||||||
|
|
||||||
|
## [下载完整示例项目](https://ext.dcloud.net.cn/plugin?name=uv-ui) <small>(请不要 下载插件ZIP)</small>
|
||||||
|
|
||||||
|
### [更多插件,请关注uv-ui组件库](https://ext.dcloud.net.cn/plugin?name=uv-ui)
|
||||||
|
|
||||||
|
<a href="https://ext.dcloud.net.cn/plugin?name=uv-ui" target="_blank">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
#### 如使用过程中有任何问题反馈,或者您对uv-ui有一些好的建议,欢迎加入uv-ui官方交流群:<a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a>
|
||||||
11
uni_modules/uv-safe-bottom/changelog.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
## 1.0.4(2023-09-14)
|
||||||
|
1. 飞书小程序支持
|
||||||
|
## 1.0.3(2023-08-14)
|
||||||
|
1. 修复百度报错的BUG
|
||||||
|
## 1.0.2(2023-07-02)
|
||||||
|
uv-safe-bottom 修复,在百度程序,抖音小程序不生效的BUG
|
||||||
|
## 1.0.1(2023-05-16)
|
||||||
|
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
|
||||||
|
2. 优化部分功能
|
||||||
|
## 1.0.0(2023-05-10)
|
||||||
|
uv-safe-bottom 底部安全区组件
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
class="uv-safe-bottom"
|
||||||
|
:style="[style]"
|
||||||
|
:class="[!isNvue && 'uv-safe-area-inset-bottom']"
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
|
||||||
|
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
|
||||||
|
/**
|
||||||
|
* SafeBottom 底部安全区
|
||||||
|
* @description 这个适配,主要是针对IPhone X等一些底部带指示条的机型,指示条的操作区域与页面底部存在重合,容易导致用户误操作,因此我们需要针对这些机型进行底部安全区适配。
|
||||||
|
* @tutorial https://www.uvui.cn/components/safeAreaInset.html
|
||||||
|
* @property {type} prop_name
|
||||||
|
* @property {Object} customStyle 定义需要用到的外部样式
|
||||||
|
*
|
||||||
|
* @event {Function()}
|
||||||
|
* @example <uv-status-bar></uv-status-bar>
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: "uv-safe-bottom",
|
||||||
|
mixins: [mpMixin, mixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
safeAreaBottomHeight: 0,
|
||||||
|
isNvue: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
style() {
|
||||||
|
const style = {};
|
||||||
|
// #ifdef APP-NVUE || MP-TOUTIAO || MP-LARK
|
||||||
|
// nvue下,高度使用js计算填充
|
||||||
|
style.height = this.$uv.addUnit(this.$uv.sys()?.safeAreaInsets?.bottom, 'px');
|
||||||
|
// #endif
|
||||||
|
return this.$uv.deepMerge(style, this.$uv.addStyle(this.customStyle));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// #ifdef APP-NVUE
|
||||||
|
// 标识为是否nvue
|
||||||
|
this.isNvue = true;
|
||||||
|
// #endif
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.uv-safe-bottom {
|
||||||
|
/* #ifndef APP-NVUE */
|
||||||
|
width: 100%;
|
||||||
|
/* #endif */
|
||||||
|
}
|
||||||
|
/* #ifndef APP-NVUE */
|
||||||
|
// 历遍生成4个方向的底部安全区
|
||||||
|
@each $d in top, right, bottom, left {
|
||||||
|
.uv-safe-area-inset-#{$d} {
|
||||||
|
padding-#{$d}: 0;
|
||||||
|
padding-#{$d}: constant(safe-area-inset-#{$d});
|
||||||
|
padding-#{$d}: env(safe-area-inset-#{$d});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* #endif */
|
||||||
|
</style>
|
||||||
87
uni_modules/uv-safe-bottom/package.json
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"id": "uv-safe-bottom",
|
||||||
|
"displayName": "uv-safe-bottom 底部安全区 全面兼容小程序、nvue、vue2、vue3等多端",
|
||||||
|
"version": "1.0.4",
|
||||||
|
"description": "这个适配,主要是针对IPhone X等一些底部带指示条的机型,指示条的操作区域与页面底部存在重合,容易导致用户误操作,因此我们需要针对这些机型进行底部安全区适配。",
|
||||||
|
"keywords": [
|
||||||
|
"uv-safe-bottom",
|
||||||
|
"uvui",
|
||||||
|
"uv-ui",
|
||||||
|
"bottom",
|
||||||
|
"底部安全区"
|
||||||
|
],
|
||||||
|
"repository": "",
|
||||||
|
"engines": {
|
||||||
|
"HBuilderX": "^3.1.0"
|
||||||
|
},
|
||||||
|
"dcloudext": {
|
||||||
|
"type": "component-vue",
|
||||||
|
"sale": {
|
||||||
|
"regular": {
|
||||||
|
"price": "0.00"
|
||||||
|
},
|
||||||
|
"sourcecode": {
|
||||||
|
"price": "0.00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"qq": ""
|
||||||
|
},
|
||||||
|
"declaration": {
|
||||||
|
"ads": "无",
|
||||||
|
"data": "插件不采集任何数据",
|
||||||
|
"permissions": "无"
|
||||||
|
},
|
||||||
|
"npmurl": ""
|
||||||
|
},
|
||||||
|
"uni_modules": {
|
||||||
|
"dependencies": [
|
||||||
|
"uv-ui-tools"
|
||||||
|
],
|
||||||
|
"encrypt": [],
|
||||||
|
"platforms": {
|
||||||
|
"cloud": {
|
||||||
|
"tcb": "y",
|
||||||
|
"aliyun": "y"
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"Vue": {
|
||||||
|
"vue2": "y",
|
||||||
|
"vue3": "y"
|
||||||
|
},
|
||||||
|
"App": {
|
||||||
|
"app-vue": "y",
|
||||||
|
"app-nvue": "y"
|
||||||
|
},
|
||||||
|
"H5-mobile": {
|
||||||
|
"Safari": "y",
|
||||||
|
"Android Browser": "y",
|
||||||
|
"微信浏览器(Android)": "y",
|
||||||
|
"QQ浏览器(Android)": "y"
|
||||||
|
},
|
||||||
|
"H5-pc": {
|
||||||
|
"Chrome": "y",
|
||||||
|
"IE": "y",
|
||||||
|
"Edge": "y",
|
||||||
|
"Firefox": "y",
|
||||||
|
"Safari": "y"
|
||||||
|
},
|
||||||
|
"小程序": {
|
||||||
|
"微信": "y",
|
||||||
|
"阿里": "y",
|
||||||
|
"百度": "y",
|
||||||
|
"字节跳动": "y",
|
||||||
|
"QQ": "y",
|
||||||
|
"钉钉": "u",
|
||||||
|
"快手": "u",
|
||||||
|
"飞书": "u",
|
||||||
|
"京东": "u"
|
||||||
|
},
|
||||||
|
"快应用": {
|
||||||
|
"华为": "u",
|
||||||
|
"联盟": "u"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
uni_modules/uv-safe-bottom/readme.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
## SafeBottom 底部安全区
|
||||||
|
|
||||||
|
> **组件名:uv-safe-bottom**
|
||||||
|
|
||||||
|
这个适配,主要是针对IPhone X等一些底部带指示条的机型,指示条的操作区域与页面底部存在重合,容易导致用户误操作,因此我们需要针对这些机型进行底部安全区适配。
|
||||||
|
|
||||||
|
### <a href="https://www.uvui.cn/guide/safeAreaInset.html" target="_blank">查看文档</a>
|
||||||
|
|
||||||
|
### [完整示例项目下载 | 关注更多组件](https://ext.dcloud.net.cn/plugin?name=uv-ui)
|
||||||
|
|
||||||
|
#### 如使用过程中有任何问题,或者您对uv-ui有一些好的建议,欢迎加入 uv-ui 交流群:<a href="https://ext.dcloud.net.cn/plugin?id=12287" target="_blank">uv-ui</a>、<a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a>
|
||||||
7
uni_modules/uv-status-bar/changelog.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
## 1.0.2(2023-06-05)
|
||||||
|
1. 兼容渐变背景色
|
||||||
|
## 1.0.1(2023-05-16)
|
||||||
|
1. 优化组件依赖,修改后无需全局引入,组件导入即可使用
|
||||||
|
2. 优化部分功能
|
||||||
|
## 1.0.0(2023-05-10)
|
||||||
|
1. 新增uv-status-bar组件
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
bgColor: {
|
||||||
|
type: String,
|
||||||
|
default: 'transparent'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<view
|
||||||
|
:style="[style]"
|
||||||
|
class="uv-status-bar"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
|
||||||
|
import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
|
||||||
|
import props from './props.js';
|
||||||
|
/**
|
||||||
|
* StatbusBar 状态栏占位
|
||||||
|
* @description 本组件主要用于状态填充,比如在自定导航栏的时候,它会自动适配一个恰当的状态栏高度。
|
||||||
|
* @tutorial https://www.uvui.cn/components/statusBar.html
|
||||||
|
* @property {String} bgColor 背景色 (默认 'transparent' )
|
||||||
|
* @property {String | Object} customStyle 自定义样式
|
||||||
|
* @example <uv-status-bar></uv-status-bar>
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: 'uv-status-bar',
|
||||||
|
mixins: [mpMixin, mixin, props],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
style() {
|
||||||
|
const style = {}
|
||||||
|
// 状态栏高度,由于某些安卓和微信开发工具无法识别css的顶部状态栏变量,所以使用js获取的方式
|
||||||
|
style.height = this.$uv.addUnit(this.$uv.sys().statusBarHeight, 'px')
|
||||||
|
if(this.bgColor){
|
||||||
|
if (this.bgColor.indexOf("gradient") > -1) {// 渐变色
|
||||||
|
style.backgroundImage = this.bgColor;
|
||||||
|
}else{
|
||||||
|
style.background = this.bgColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.$uv.deepMerge(style, this.$uv.addStyle(this.customStyle))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.uv-status-bar {
|
||||||
|
// nvue会默认100%,如果nvue下,显式写100%的话,会导致宽度不为100%而异常
|
||||||
|
/* #ifndef APP-NVUE */
|
||||||
|
width: 100%;
|
||||||
|
/* #endif */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
87
uni_modules/uv-status-bar/package.json
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"id": "uv-status-bar",
|
||||||
|
"displayName": "uv-status-bar 状态栏占位",
|
||||||
|
"version": "1.0.2",
|
||||||
|
"description": "状态栏占位组件主要用于状态填充,比如在自定导航栏的时候,它会自动适配一个恰当的状态栏高度。",
|
||||||
|
"keywords": [
|
||||||
|
"uv-status-bar",
|
||||||
|
"uvui",
|
||||||
|
"uv-ui",
|
||||||
|
"status-bar",
|
||||||
|
"状态栏"
|
||||||
|
],
|
||||||
|
"repository": "",
|
||||||
|
"engines": {
|
||||||
|
"HBuilderX": "^3.1.0"
|
||||||
|
},
|
||||||
|
"dcloudext": {
|
||||||
|
"type": "component-vue",
|
||||||
|
"sale": {
|
||||||
|
"regular": {
|
||||||
|
"price": "0.00"
|
||||||
|
},
|
||||||
|
"sourcecode": {
|
||||||
|
"price": "0.00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"qq": ""
|
||||||
|
},
|
||||||
|
"declaration": {
|
||||||
|
"ads": "无",
|
||||||
|
"data": "插件不采集任何数据",
|
||||||
|
"permissions": "无"
|
||||||
|
},
|
||||||
|
"npmurl": ""
|
||||||
|
},
|
||||||
|
"uni_modules": {
|
||||||
|
"dependencies": [
|
||||||
|
"uv-ui-tools"
|
||||||
|
],
|
||||||
|
"encrypt": [],
|
||||||
|
"platforms": {
|
||||||
|
"cloud": {
|
||||||
|
"tcb": "y",
|
||||||
|
"aliyun": "y"
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"Vue": {
|
||||||
|
"vue2": "y",
|
||||||
|
"vue3": "y"
|
||||||
|
},
|
||||||
|
"App": {
|
||||||
|
"app-vue": "y",
|
||||||
|
"app-nvue": "y"
|
||||||
|
},
|
||||||
|
"H5-mobile": {
|
||||||
|
"Safari": "y",
|
||||||
|
"Android Browser": "y",
|
||||||
|
"微信浏览器(Android)": "y",
|
||||||
|
"QQ浏览器(Android)": "y"
|
||||||
|
},
|
||||||
|
"H5-pc": {
|
||||||
|
"Chrome": "y",
|
||||||
|
"IE": "y",
|
||||||
|
"Edge": "y",
|
||||||
|
"Firefox": "y",
|
||||||
|
"Safari": "y"
|
||||||
|
},
|
||||||
|
"小程序": {
|
||||||
|
"微信": "y",
|
||||||
|
"阿里": "y",
|
||||||
|
"百度": "y",
|
||||||
|
"字节跳动": "y",
|
||||||
|
"QQ": "y",
|
||||||
|
"钉钉": "u",
|
||||||
|
"快手": "u",
|
||||||
|
"飞书": "u",
|
||||||
|
"京东": "u"
|
||||||
|
},
|
||||||
|
"快应用": {
|
||||||
|
"华为": "u",
|
||||||
|
"联盟": "u"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
uni_modules/uv-status-bar/readme.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
## StatbusBar 状态栏占位
|
||||||
|
|
||||||
|
> **组件名:uv-status-bar**
|
||||||
|
|
||||||
|
本组件主要用于状态填充,比如在自定导航栏的时候,它会自动适配一个恰当的状态栏高度。
|
||||||
|
|
||||||
|
### [完整示例项目下载 | 关注更多组件](https://ext.dcloud.net.cn/plugin?name=uv-ui)
|
||||||
|
|
||||||
|
#### 如使用过程中有任何问题,或者您对uv-ui有一些好的建议,欢迎加入 uv-ui 交流群:<a href="https://ext.dcloud.net.cn/plugin?id=12287" target="_blank">uv-ui</a>、<a href="https://www.uvui.cn/components/addQQGroup.html" target="_blank">官方QQ群</a>
|
||||||
|
|
||||||
2
uni_modules/uv-toolbar/changelog.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
## 1.0.0(2023-08-02)
|
||||||
|
1. 新增工具条组件
|
||||||