add-relation.vue 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. <template>
  2. <view class="page-add-relation">
  3. <view class="form-card">
  4. <!-- Relation Type -->
  5. <view class="form-group">
  6. <text class="form-label">关系类型 *</text>
  7. <view class="type-grid">
  8. <view
  9. v-for="opt in relationTypeOptions"
  10. :key="opt.value"
  11. class="type-chip"
  12. :class="{ active: form.relationType === opt.value }"
  13. @click="form.relationType = opt.value"
  14. >
  15. <text class="chip-text">{{ opt.label }}</text>
  16. </view>
  17. </view>
  18. </view>
  19. <!-- Name -->
  20. <view class="form-group">
  21. <text class="form-label">姓名</text>
  22. <input
  23. v-model="form.name"
  24. class="form-input"
  25. placeholder="请输入姓名"
  26. placeholder-class="input-placeholder"
  27. maxlength="20"
  28. />
  29. </view>
  30. <!-- Birth Date -->
  31. <view class="form-group">
  32. <text class="form-label">生日 *</text>
  33. <picker mode="date" :value="form.birthDate" @change="onBirthDateChange">
  34. <view class="form-input picker-trigger">
  35. <text :class="{ 'text-placeholder': !form.birthDate }">
  36. {{ form.birthDate || '请选择出生日期' }}
  37. </text>
  38. </view>
  39. </picker>
  40. </view>
  41. <!-- Gender -->
  42. <view class="form-group">
  43. <text class="form-label">性别</text>
  44. <view class="gender-row">
  45. <view
  46. v-for="opt in genderOptions"
  47. :key="opt.value"
  48. class="gender-option"
  49. :class="{ active: form.gender === opt.value }"
  50. @click="form.gender = opt.value"
  51. >
  52. <text class="gender-text">{{ opt.label }}</text>
  53. </view>
  54. </view>
  55. </view>
  56. <!-- Note -->
  57. <view class="form-group">
  58. <text class="form-label">备注</text>
  59. <input
  60. v-model="form.note"
  61. class="form-input"
  62. placeholder="选填,如:工作中的合作伙伴"
  63. placeholder-class="input-placeholder"
  64. maxlength="50"
  65. />
  66. </view>
  67. </view>
  68. <!-- Save Button -->
  69. <view class="save-bar">
  70. <view class="btn-save" :class="{ saving: saving }" @click="handleSave">
  71. <text class="btn-save-text">{{ saving ? '保存中...' : '保存' }}</text>
  72. </view>
  73. </view>
  74. </view>
  75. </template>
  76. <script setup>
  77. import { ref, reactive } from 'vue'
  78. import { onLoad } from '@dcloudio/uni-app'
  79. import requestInstance from '@/utils/api'
  80. const relationApi = {
  81. create: (data) => requestInstance.post('/relation/create', data),
  82. update: (data) => requestInstance.post('/relation/update', data),
  83. get: (id) => requestInstance.post('/relation/get', { id })
  84. }
  85. const relationTypeOptions = [
  86. { value: 'spouse', label: '夫妻' },
  87. { value: 'parent_child', label: '亲子' },
  88. { value: 'colleague', label: '同事' },
  89. { value: 'friend', label: '朋友' },
  90. { value: 'superior', label: '上下级' }
  91. ]
  92. const genderOptions = [
  93. { value: 0, label: '保密' },
  94. { value: 1, label: '男' },
  95. { value: 2, label: '女' }
  96. ]
  97. const form = reactive({
  98. relationType: '',
  99. name: '',
  100. birthDate: '',
  101. gender: 0,
  102. note: ''
  103. })
  104. const isEdit = ref(false)
  105. const editId = ref(null)
  106. const saving = ref(false)
  107. onLoad((query) => {
  108. if (query && query.id) {
  109. isEdit.value = true
  110. editId.value = query.id
  111. uni.setNavigationBarTitle({ title: '编辑关系人' })
  112. fetchRelation(query.id)
  113. }
  114. })
  115. async function fetchRelation(id) {
  116. try {
  117. const data = await relationApi.get(id)
  118. if (data) {
  119. form.relationType = data.relationType || ''
  120. form.name = data.name || ''
  121. form.birthDate = data.birthDate || ''
  122. form.gender = data.gender ?? 0
  123. form.note = data.note || ''
  124. }
  125. } catch (e) {
  126. uni.showToast({ title: '加载关系人信息失败', icon: 'none' })
  127. }
  128. }
  129. function onBirthDateChange(e) {
  130. form.birthDate = e.detail.value
  131. }
  132. function validate() {
  133. if (!form.relationType) {
  134. uni.showToast({ title: '请选择关系类型', icon: 'none' })
  135. return false
  136. }
  137. if (!form.birthDate) {
  138. uni.showToast({ title: '请选择出生日期', icon: 'none' })
  139. return false
  140. }
  141. return true
  142. }
  143. async function handleSave() {
  144. if (!validate()) return
  145. if (saving.value) return
  146. saving.value = true
  147. try {
  148. if (isEdit.value) {
  149. await relationApi.update({ id: editId.value, ...form })
  150. uni.showToast({ title: '更新成功', icon: 'success' })
  151. } else {
  152. await relationApi.create(form)
  153. uni.showToast({ title: '添加成功', icon: 'success' })
  154. }
  155. setTimeout(() => {
  156. uni.navigateBack()
  157. }, 1200)
  158. } catch (e) {
  159. // error toast already handled by requestInstance
  160. } finally {
  161. saving.value = false
  162. }
  163. }
  164. </script>
  165. <style scoped lang="scss">
  166. @import "@/styles/theme.scss";
  167. .page-add-relation {
  168. min-height: 100vh;
  169. background: var(--color-bg);
  170. padding: 16px;
  171. }
  172. /* Form card */
  173. .form-card {
  174. background: var(--color-bg-card);
  175. border-radius: 16px;
  176. padding: 24px 20px;
  177. box-shadow: var(--shadow-card);
  178. }
  179. .form-group {
  180. margin-bottom: 24px;
  181. }
  182. .form-group:last-child {
  183. margin-bottom: 0;
  184. }
  185. .form-label {
  186. display: block;
  187. font-size: 14px;
  188. font-weight: 600;
  189. color: var(--color-text-secondary);
  190. margin-bottom: 10px;
  191. }
  192. /* Input fields */
  193. .form-input {
  194. width: 100%;
  195. height: 44px;
  196. background: var(--color-bg-card);
  197. border: 1px solid var(--color-border);
  198. border-radius: 10px;
  199. padding: 0 14px;
  200. font-size: 15px;
  201. color: var(--color-text-primary);
  202. box-sizing: border-box;
  203. }
  204. .form-input:focus {
  205. border-color: var(--color-brand);
  206. box-shadow: 0 0 0 2px rgba(30, 58, 95, 0.1);
  207. }
  208. .input-placeholder {
  209. color: var(--color-text-secondary);
  210. font-size: 15px;
  211. }
  212. /* Picker */
  213. .picker-trigger {
  214. display: flex;
  215. align-items: center;
  216. }
  217. .text-placeholder {
  218. color: var(--color-text-secondary);
  219. font-size: 15px;
  220. }
  221. /* Relation type chips */
  222. .type-grid {
  223. display: flex;
  224. flex-wrap: wrap;
  225. gap: 10px;
  226. }
  227. .type-chip {
  228. padding: 10px 20px;
  229. border: 1px solid var(--color-border);
  230. border-radius: 10px;
  231. background: var(--color-bg-card);
  232. text-align: center;
  233. flex: 0 0 auto;
  234. min-width: 80px;
  235. }
  236. .type-chip:active {
  237. transform: scale(0.97);
  238. }
  239. .type-chip.active {
  240. border-color: var(--color-brand);
  241. background: var(--color-brand);
  242. }
  243. .type-chip.active .chip-text {
  244. color: var(--color-bg-card);
  245. }
  246. .chip-text {
  247. font-size: 14px;
  248. font-weight: 500;
  249. color: var(--color-text-secondary);
  250. }
  251. /* Gender row */
  252. .gender-row {
  253. display: flex;
  254. gap: 12px;
  255. }
  256. .gender-option {
  257. flex: 1;
  258. padding: 10px 0;
  259. border: 1px solid var(--color-border);
  260. border-radius: 10px;
  261. text-align: center;
  262. background: var(--color-bg-card);
  263. }
  264. .gender-option:active {
  265. transform: scale(0.97);
  266. }
  267. .gender-option.active {
  268. border-color: var(--color-brand);
  269. background: var(--color-brand);
  270. }
  271. .gender-option.active .gender-text {
  272. color: var(--color-bg-card);
  273. }
  274. .gender-text {
  275. font-size: 14px;
  276. font-weight: 500;
  277. color: var(--color-text-secondary);
  278. }
  279. /* Save button */
  280. .save-bar {
  281. margin-top: 24px;
  282. padding-bottom: 16px;
  283. }
  284. .btn-save {
  285. width: 100%;
  286. height: 48px;
  287. background: var(--color-brand);
  288. border-radius: 12px;
  289. display: flex;
  290. align-items: center;
  291. justify-content: center;
  292. }
  293. .btn-save:active {
  294. transform: scale(0.97);
  295. opacity: 0.9;
  296. }
  297. .btn-save.saving {
  298. opacity: 0.7;
  299. pointer-events: none;
  300. }
  301. .btn-save-text {
  302. color: var(--color-bg-card);
  303. font-size: 16px;
  304. font-weight: 600;
  305. }
  306. </style>