ActivityCard.vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. <template>
  2. <uni-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8">
  3. <view class="active-box">
  4. <!-- 热门标签 -->
  5. <view v-if="config.hot" class="btn-tag-star">
  6. <uni-icons type="star-filled" size="16" color="#ffd700"></uni-icons>
  7. </view>
  8. <!-- 图片区域 -->
  9. <view class="img crm-cursor" @click="handleClick">
  10. <image :src="imageSrc" mode="widthFix" />
  11. </view>
  12. <!-- 内容区域 -->
  13. <view class="content">
  14. <view class="content-box">
  15. <view v-if="config.time" class="time">{{ config.time }}</view>
  16. <view class="title" @click="handleClick">{{ t(config.title) }}</view>
  17. <view v-if="config.description" class="des crm-one-font">{{ t(config.description) }}</view>
  18. <!-- 按钮区域 -->
  19. <view class="bottom">
  20. <template v-for="(btn, index) in config.buttons" :key="index">
  21. <text v-if="shouldShowButton(btn)" :class="['btn', getButtonType(btn)]"
  22. @click="handleButtonClick(btn)">
  23. {{ t(btn.text) }}
  24. <template v-if="btn.suffix && state[btn.suffix]">
  25. {{ state[btn.suffix] }}
  26. </template>
  27. <template v-if="btn.suffixText">
  28. {{ t(btn.suffixText) }}
  29. </template>
  30. </text>
  31. <text v-else-if="btn.elseType && btn.condition" :class="['btn', btn.elseType]">
  32. {{ t(btn.text) }}
  33. </text>
  34. </template>
  35. </view>
  36. </view>
  37. </view>
  38. </view>
  39. </uni-col>
  40. </template>
  41. <script setup lang="ts">
  42. import { computed } from 'vue'
  43. import { useI18n } from 'vue-i18n'
  44. const { t, locale } = useI18n()
  45. const props = defineProps<{
  46. config: any
  47. state?: Record<string, any>
  48. }>()
  49. const emit = defineEmits(['action'])
  50. // 图片源
  51. const imageSrc = computed(() => {
  52. if (typeof props.config.image === 'object') {
  53. const lang = locale.value || 'en'
  54. return props.config.image[lang] || props.config.image.default
  55. }
  56. return props.config.image
  57. })
  58. // 判断按钮是否显示
  59. const shouldShowButton = (btn: any): boolean => {
  60. if (!btn.condition) return true
  61. if (!props.state) return false
  62. try {
  63. const conditionStr = btn.condition
  64. const evalStr = conditionStr.replace(/([a-zA-Z_][a-zA-Z0-9_.]*)/g, (match) => {
  65. if (match.includes('.')) {
  66. const parts = match.split('.')
  67. let value: any = props.state
  68. for (const part of parts) {
  69. value = value?.[part]
  70. }
  71. return JSON.stringify(value)
  72. }
  73. return JSON.stringify(props.state[match])
  74. })
  75. const fn = new Function(`return ${evalStr}`)
  76. return fn()
  77. } catch (error) {
  78. console.error('按钮条件评估失败:', error, btn.condition)
  79. return false
  80. }
  81. }
  82. // 判断按钮是否灰色
  83. const shouldClassCondition = (btn: any): boolean => {
  84. if (!btn.classCondition) return true
  85. if (!props.state) return false
  86. try {
  87. const conditionStr = btn.classCondition
  88. const evalStr = conditionStr.replace(/([a-zA-Z_][a-zA-Z0-9_.]*)/g, (match) => {
  89. if (match.includes('.')) {
  90. const parts = match.split('.')
  91. let value: any = props.state
  92. for (const part of parts) {
  93. value = value?.[part]
  94. }
  95. return JSON.stringify(value)
  96. }
  97. return JSON.stringify(props.state[match])
  98. })
  99. const fn = new Function(`return ${evalStr}`)
  100. return fn()
  101. } catch (error) {
  102. console.error('按钮条件评估失败:', error, btn.condition)
  103. return false
  104. }
  105. }
  106. // 获取按钮类型
  107. const getButtonType = (btn: any): string => {
  108. if (btn.type === 'dynamic') {
  109. return shouldShowButton(btn) && shouldClassCondition(btn) ? 'red' : 'red gray'
  110. }
  111. return btn.type
  112. }
  113. // 处理点击事件
  114. const handleClick = () => {
  115. if (props.config.onClick) {
  116. emit('action', {
  117. type: props.config.onClick,
  118. params: props.config.onClickParams || []
  119. })
  120. }
  121. }
  122. // 处理按钮点击
  123. const handleButtonClick = (btn: any) => {
  124. if (btn.action && btn.action !== 'disabled') {
  125. emit('action', {
  126. type: btn.action,
  127. params: btn.params || []
  128. })
  129. }
  130. }
  131. </script>
  132. <style scoped lang="scss">
  133. @import "@/uni.scss";
  134. .active-box {
  135. width: 100%;
  136. position: relative;
  137. margin-bottom: px2rpx(20);
  138. padding: 0;
  139. aspect-ratio: 3 / 4;
  140. .btn-tag-star {
  141. position: absolute;
  142. top: px2rpx(30);
  143. left: px2rpx(30);
  144. z-index: 2;
  145. background: linear-gradient(135deg, #ffd700 0%, #ffb347 100%);
  146. padding: px2rpx(12) px2rpx(28);
  147. border-radius: px2rpx(40);
  148. font-size: px2rpx(26);
  149. font-weight: bold;
  150. color: #0f1423;
  151. display: flex;
  152. align-items: center;
  153. gap: px2rpx(8);
  154. box-shadow: 0 px2rpx(4) px2rpx(12) rgba(255, 215, 0, 0.3);
  155. letter-spacing: px2rpx(1);
  156. text-transform: uppercase;
  157. .uni-icons {
  158. font-size: px2rpx(28) !important;
  159. }
  160. }
  161. .img {
  162. width: 100%;
  163. position: relative;
  164. overflow: hidden;
  165. image {
  166. width: 100%;
  167. object-fit: cover;
  168. transition: transform 0.6s ease;
  169. }
  170. }
  171. .content {
  172. width: 100%;
  173. padding: 0 px2rpx(14);
  174. box-sizing: border-box;
  175. margin-top: px2rpx(-20);
  176. .content-box {
  177. width: 100%;
  178. height: 100%;
  179. display: flex;
  180. flex-direction: column;
  181. padding: 0 px2rpx(14);
  182. box-sizing: border-box;
  183. background-color: var(--color-white);
  184. z-index: 1;
  185. position: relative;
  186. }
  187. .title {
  188. width: 100%;
  189. margin: px2rpx(20) 0;
  190. font-size: px2rpx(24);
  191. font-weight: 700;
  192. color: var(--color-error);
  193. line-height: 1.3;
  194. display: block;
  195. margin-bottom: px2rpx(12);
  196. overflow: hidden;
  197. text-overflow: ellipsis;
  198. white-space: nowrap;
  199. }
  200. .time {
  201. margin-top: px2rpx(20);
  202. width: fit-content;
  203. font-size: px2rpx(12);
  204. color: var(--color-error);
  205. background-color:
  206. color-mix(in oklab, var(--color-error) 10%, transparent);
  207. line-height: px2rpx(20);
  208. padding: px2rpx(5) px2rpx(7);
  209. border-radius: px2rpx(2);
  210. }
  211. .des {
  212. width: 100%;
  213. font-size: px2rpx(14);
  214. color: var(--color-slate-900);
  215. margin-bottom: px2rpx(40);
  216. display: -webkit-box;
  217. -webkit-line-clamp: 2;
  218. -webkit-box-orient: vertical;
  219. overflow: hidden;
  220. line-height: 1.2;
  221. }
  222. .bottom {
  223. display: flex;
  224. flex-wrap: wrap;
  225. align-items: center;
  226. gap: px2rpx(20);
  227. .btn {
  228. color: var(--color-white);
  229. padding: px2rpx(16);
  230. font-size: px2rpx(14);
  231. font-weight: 600;
  232. transition: all 0.3s ease;
  233. display: inline-flex;
  234. align-items: center;
  235. justify-content: center;
  236. white-space: nowrap;
  237. letter-spacing: px2rpx(1);
  238. text-transform: uppercase;
  239. padding: px2rpx(8) px2rpx(16);
  240. box-sizing: border-box;
  241. cursor: pointer;
  242. &.red {
  243. width: 100%;
  244. background-color: var(--color-secondary);
  245. }
  246. &.check {
  247. font-size: px2rpx(11);
  248. background-color: var(--color-error);
  249. }
  250. &.gray {
  251. cursor: not-allowed;
  252. background-color: var(--color-navy-200);
  253. }
  254. }
  255. }
  256. }
  257. // 列表项样式(用于底部链接)
  258. .link-list {
  259. display: flex;
  260. gap: px2rpx(30);
  261. flex-wrap: wrap;
  262. margin-top: px2rpx(30);
  263. padding-top: px2rpx(30);
  264. border-top: px2rpx(1) solid rgba(255, 215, 0, 0.2);
  265. .link-item {
  266. color: #8e9aaf;
  267. font-size: px2rpx(26);
  268. transition: color 0.3s;
  269. &:active {
  270. color: #ffd700;
  271. }
  272. &::before {
  273. content: '•';
  274. color: #ffd700;
  275. margin-right: px2rpx(8);
  276. }
  277. }
  278. }
  279. }
  280. // 针对不同屏幕尺寸的响应式调整
  281. @media screen and (max-width: 767px) {
  282. .active-box {
  283. .title {
  284. width: px2rpx(200) !important;
  285. }
  286. }
  287. }
  288. // 添加悬停效果(仅H5)
  289. /* #ifdef H5 */
  290. .active-box:hover {
  291. // transform: translateY(-6rpx);
  292. // box-shadow: 0 p 60rpx rgba(0, 0, 0, 0.5);
  293. // transition: all 0.3s ease;
  294. .img image {
  295. transform: scale(1.05);
  296. }
  297. }
  298. /* #endif */
  299. </style>